Perplexity helper

Simple script that adds buttons to Perplexity website for repeating request using Copilot.

  1. // ==UserScript==
  2. // @name Perplexity helper
  3. // @namespace Tiartyos
  4. // @match https://www.perplexity.ai/*
  5. // @grant none
  6. // @version 6.3
  7. // @author Tiartyos, monnef
  8. // @description Simple script that adds buttons to Perplexity website for repeating request using Copilot.
  9. // @require https://code.jquery.com/jquery-3.6.0.min.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/lodash-fp/0.10.4/lodash-fp.min.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js
  13. // @require https://cdn.jsdelivr.net/npm/color2k@2.0.2/dist/index.unpkg.umd.js
  14. // @require https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/jsondiffpatch/0.4.1/jsondiffpatch.umd.min.js
  16. // @require https://cdn.jsdelivr.net/npm/hex-to-css-filter@6.0.0/dist/umd/hex-to-css-filter.min.js
  17. // @require https://cdn.jsdelivr.net/npm/perplex-plus@0.0.40/dist/lib/perplex-plus.js
  18. // @homepageURL https://www.perplexity.ai/
  19. // @license GPL-3.0-or-later
  20. // ==/UserScript==
  21.  
  22. if (EventTarget.prototype.original_addEventListener == null) {
  23. EventTarget.prototype.original_addEventListener = EventTarget.prototype.addEventListener;
  24.  
  25. const interestedIn = {
  26. typSubstring: [
  27. 'key',
  28. ],
  29. nodeName: [
  30. // 'TEXTAREA',
  31. // 'INPUT',
  32. // 'BODY',
  33. ],
  34. }
  35.  
  36. function addEventListener_hook(typ, fn, opt) {
  37. // console.log('--- add event listener', { nodeName: this.nodeName, typ, fn, opt, el: this });
  38. this.all_handlers = this.all_handlers || [];
  39. this.all_handlers.push({ typ, fn, opt });
  40. this.original_addEventListener(typ, fn, opt);
  41.  
  42. if (interestedIn.typSubstring.some(s => typ.includes(s)) || interestedIn.nodeName.includes(this.nodeName)) {
  43. debugLog('!!! added event listener', { typ, fn, opt, el: this });
  44. }
  45. }
  46.  
  47. EventTarget.prototype.addEventListener = addEventListener_hook;
  48. }
  49.  
  50. const PP = window.PP.noConflict();
  51. const jq = PP.jq;
  52. const hexToCssFilter = window.HexToCSSFilter.hexToCSSFilter;
  53.  
  54. const $c = (cls, parent) => jq(`.${cls}`, parent);
  55. const $i = (id, parent) => jq(`#${id}`, parent);
  56. const takeStr = n => str => str.slice(0, n);
  57. const dropStr = n => str => str.slice(n);
  58. const dropRightStr = n => str => str.slice(0, -n);
  59. const filter = pred => xs => xs.filter(pred);
  60. const pipe = x => (...fns) => fns.reduce((acc, fn) => fn(acc), x);
  61.  
  62. const nl = '\n';
  63. const markdownConverter = new showdown.Converter({ tables: true });
  64.  
  65. let debugMode = false;
  66. const enableDebugMode = () => {
  67. debugMode = true;
  68. };
  69.  
  70. const userscriptName = 'Perplexity helper';
  71. const logPrefix = `[${userscriptName}]`;
  72.  
  73. const debugLog = (...args) => {
  74. if (debugMode) {
  75. console.debug(logPrefix, ...args);
  76. }
  77. };
  78.  
  79. let debugTags = false;
  80. const debugLogTags = (...args) => {
  81. if (debugTags) {
  82. console.debug(logPrefix, '[tags]', ...args);
  83. }
  84. };
  85.  
  86. const log = (...args) => {
  87. console.log(logPrefix, ...args);
  88. };
  89.  
  90. const logError = (...args) => {
  91. console.error(logPrefix, ...args);
  92. };
  93.  
  94. const enableTagsDebugging = () => {
  95. debugTags = true;
  96. };
  97.  
  98. ($ => {
  99. $.fn.nthParent = function (n) {
  100. let $p = $(this);
  101. if (!(n > -0)) { return $(); }
  102. let p = 1 + n;
  103. while (p--) { $p = $p.parent(); }
  104. return $p;
  105. };
  106. })(jq);
  107.  
  108. // unpkg had quite often problems, tens of seconds to load, sometime 503 fails
  109. // const getLucideIconUrl = iconName => `https://unpkg.com/lucide-static@latest/icons/${iconName}.svg`;
  110. const getLucideIconUrl = (iconName) => `https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/${iconName}.svg`;
  111.  
  112. const getBrandIconInfo = (modelName = '') => {
  113. const normalizedModelName = modelName.toLowerCase();
  114.  
  115. if (normalizedModelName.includes('claude')) {
  116. return { iconName: 'claude', brandColor: '#D97757' };
  117. } else if (normalizedModelName.includes('gpt') || normalizedModelName.startsWith('o')) {
  118. return { iconName: 'openai', brandColor: '#FFFFFF' };
  119. } else if (normalizedModelName.includes('gemini')) {
  120. return { iconName: 'gemini', brandColor: '#1C69FF' };
  121. } else if (normalizedModelName.includes('sonar') || normalizedModelName.includes('r1') || normalizedModelName.includes('best') || normalizedModelName.includes('auto')) {
  122. return { iconName: 'perplexity', brandColor: '#22B8CD' };
  123. } else if (normalizedModelName.includes('grok')) {
  124. return { iconName: 'xai', brandColor: '#FFFFFF' };
  125. } else if (normalizedModelName.includes('llama') || normalizedModelName.includes('meta')) {
  126. return { iconName: 'meta', brandColor: '#1D65C1' };
  127. } else if (normalizedModelName.includes('anthropic')) {
  128. return { iconName: 'anthropic', brandColor: '#F1F0E8' };
  129. }
  130.  
  131. return null;
  132. };
  133.  
  134. const getTDesignIconUrl = iconName => `https://api.iconify.design/tdesign:${iconName}.svg`;
  135.  
  136. const getLobeIconsUrl = iconName => `https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/${iconName}.svg`;
  137.  
  138. const parseIconName = iconName => {
  139. if (!iconName.includes(':')) return { typePrefix: 'l', processedIconName: iconName };
  140. const [typePrefix, processedIconName] = iconName.split(':');
  141. return { typePrefix, processedIconName };
  142. };
  143.  
  144. const getIconUrl = iconName => {
  145. const { typePrefix, processedIconName } = parseIconName(iconName);
  146. if (typePrefix === 'td') {
  147. return getTDesignIconUrl(processedIconName);
  148. }
  149. if (typePrefix === 'l') {
  150. return getLucideIconUrl(processedIconName);
  151. }
  152. throw new Error(`Unknown icon type: ${typePrefix}`);
  153. };
  154.  
  155. const pplxHelperTag = 'pplx-helper';
  156. const genCssName = x => `${pplxHelperTag}--${x}`;
  157.  
  158. 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}" >
  159. <div class="flex items-center leading-none justify-center gap-xs">
  160. ${icoName}
  161. </div></button>`;
  162.  
  163. const upperButton = (id, icoName, title) => `
  164. <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>
  165. `;
  166.  
  167. const textButton = (id, text, title) => `
  168. <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">
  169. <div class="flex items-center leading-none justify-center gap-xs"><span class="flex items-center relative ">${text}</span></div></button>
  170. `;
  171. const icoColor = '#1F1F1F';
  172. 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>`;
  173. 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;"/>
  174. <g transform="matrix(14.135,0,0,14.135,329.029,-28.2701)">
  175. <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;"/>
  176. </g></svg>`;
  177.  
  178. 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"
  179. \t xml:space="preserve">
  180. <g>
  181. \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
  182. \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
  183. \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
  184. \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
  185. \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
  186. \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
  187. \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
  188. \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
  189. \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
  190. \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
  191. \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
  192. \t\tC32.443,9,30.766,10.677,28.701,10.677z"/>
  193. </g>
  194. </svg>`;
  195.  
  196.  
  197. const perplexityHelperModalId = 'perplexityHelperModal';
  198. const getPerplexityHelperModal = () => $i(perplexityHelperModalId);
  199.  
  200. const modalSettingsTitleCls = genCssName('modal-settings-title');
  201.  
  202. const gitlabLogo = classes => `
  203. <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>
  204. `;
  205.  
  206. const modalLargeIconAnchorClasses = 'hover:scale-110 opacity-50 hover:opacity-100 transition-all duration-300';
  207.  
  208. const modalTabGroupTabsCls = genCssName('modal-tab-group-tabs');
  209. const modalTabGroupActiveCls = genCssName('modal-tab-group-active');
  210. const modalTabGroupContentCls = genCssName('modal-tab-group-content');
  211. const modalTabGroupSeparatorCls = genCssName('modal-tab-group-separator');
  212.  
  213. const modalHTML = `
  214. <div id="${perplexityHelperModalId}" class="modal">
  215. <div class="modal-content">
  216. <span class="close">&times;</span>
  217. <h1 class="flex items-center gap-4">
  218. <span class="mr-4 ${modalSettingsTitleCls}">Perplexity Helper</span>
  219. <a href="https://gitlab.com/Tiartyos/perplexity-helper"
  220. target="_blank" title="GitLab Repository"
  221. class="${modalLargeIconAnchorClasses}"
  222. >
  223. ${gitlabLogo('w-8 h-8 invert')}
  224. </a>
  225. <a href="https://tiartyos.gitlab.io/perplexity-helper/"
  226. target="_blank" title="Web Page"
  227. class="${modalLargeIconAnchorClasses}"
  228. >
  229. <img src="${getLucideIconUrl('globe')}" class="w-8 h-8 invert">
  230. </a>
  231. </h1>
  232. <p class="text-xs opacity-30 mt-1 mb-3">Changes may require page refresh.</p>
  233. <div class="${modalTabGroupTabsCls}">
  234. </div>
  235. <hr class="!mt-0 !mb-0 ${modalTabGroupSeparatorCls}">
  236. </div>
  237. </div>
  238. `;
  239.  
  240. const tagsContainerCls = genCssName('tags-container');
  241. const tagContainerCompactCls = genCssName('tag-container-compact');
  242. const tagContainerWiderCls = genCssName('tag-container-wider');
  243. const tagContainerWideCls = genCssName('tag-container-wide');
  244. const tagContainerExtraWideCls = genCssName('tag-container-extra-wide');
  245. const threadTagContainerCls = genCssName('thread-tag-container');
  246. const newTagContainerCls = genCssName('new-tag-container');
  247. const newTagContainerInCollectionCls = genCssName('new-tag-container-in-collection');
  248. const tagCls = genCssName('tag');
  249. const tagDarkTextCls = genCssName('tag-dark-text');
  250. const tagIconCls = genCssName('tag-icon');
  251. const tagPaletteCls = genCssName('tag-palette');
  252. const tagPaletteItemCls = genCssName('tag-palette-item');
  253. const tagTweakNoBorderCls = genCssName('tag-tweak-no-border');
  254. const tagTweakSlimPaddingCls = genCssName('tag-tweak-slim-padding');
  255. const tagsPreviewCls = genCssName('tags-preview');
  256. const tagsPreviewNewCls = genCssName('tags-preview-new');
  257. const tagsPreviewThreadCls = genCssName('tags-preview-thread');
  258. const tagsPreviewNewInCollectionCls = genCssName('tags-preview-new-in-collection');
  259. const tagTweakTextShadowCls = genCssName('tag-tweak-text-shadow');
  260. const tagFenceCls = genCssName('tag-fence');
  261. const tagAllFencesWrapperCls = genCssName('tag-all-fences-wrapper');
  262. const tagRestOfTagsWrapperCls = genCssName('tag-rest-of-tags-wrapper');
  263. const tagFenceContentCls = genCssName('tag-fence-content');
  264. const tagDirectoryCls = genCssName('tag-directory');
  265. const tagDirectoryContentCls = genCssName('tag-directory-content');
  266. const helpTextCls = genCssName('help-text');
  267. const queryBoxCls = genCssName('query-box');
  268. const controlsAreaCls = genCssName('controls-area');
  269. const textAreaCls = genCssName('text-area');
  270. const standardButtonCls = genCssName('standard-button');
  271. const lucideIconParentCls = genCssName('lucide-icon-parent');
  272. const roundedMD = genCssName('rounded-md');
  273. const leftPanelSlimCls = genCssName('left-panel-slim');
  274. const modelIconButtonCls = genCssName('model-icon-button');
  275. const modelLabelCls = genCssName('model-label');
  276. const modelLabelStyleJustTextCls = genCssName('model-label-style-just-text');
  277. const modelLabelStyleButtonSubtleCls = genCssName('model-label-style-button-subtle');
  278. const modelLabelStyleButtonWhiteCls = genCssName('model-label-style-button-white');
  279. const modelLabelStyleButtonCyanCls = genCssName('model-label-style-button-cyan');
  280. const modelLabelOverwriteCyanIconToGrayCls = genCssName('model-label-overwrite-cyan-icon-to-gray');
  281. const modelLabelRemoveCpuIconCls = genCssName('model-label-remove-cpu-icon');
  282. const reasoningModelCls = genCssName('reasoning-model');
  283. const modelLabelLargerIconsCls = genCssName('model-label-larger-icons');
  284. const notReasoningModelCls = genCssName('not-reasoning-model');
  285. const modelIconCls = genCssName('model-icon');
  286. const iconColorCyanCls = genCssName('icon-color-cyan');
  287. const iconColorGrayCls = genCssName('icon-color-gray');
  288. const iconColorWhiteCls = genCssName('icon-color-white');
  289. const errorIconCls = genCssName('error-icon');
  290. const customJsAppliedCls = genCssName('customJsApplied');
  291. const customCssAppliedCls = genCssName('customCssApplied');
  292. const customWidgetsHtmlAppliedCls = genCssName('customWidgetsHtmlApplied');
  293. const sideMenuHiddenCls = genCssName('side-menu-hidden');
  294. const sideMenuLabelsHiddenCls = genCssName('side-menu-labels-hidden');
  295. const topSettingsButtonId = genCssName('settings-button-top');
  296. const leftSettingsButtonId = genCssName('settings-button-left');
  297. const leftSettingsButtonWrapperId = genCssName('settings-button-left-wrapper');
  298. const leftMarginOfThreadContentStylesId = genCssName('left-margin-of-thread-content-styles');
  299. const enhancedSubmitButtonCls = genCssName('enhanced-submit-button');
  300. const enhancedSubmitButtonPhTextCls = genCssName('enhanced-submit-button-ph-text');
  301. const enhancedSubmitButtonActiveCls = genCssName('enhanced-submit-button-active'); // Added proper generated class name
  302. const promptAreaKeyListenerCls = genCssName('prompt-area-key-listener');
  303. const promptAreaKeyListenerIndicatorCls = genCssName('prompt-area-key-listener-indicator');
  304. const pulseFocusCls = genCssName('pulse-focus');
  305.  
  306. const cyanPerplexityColor = '#1fb8cd';
  307. const cyanMediumPerplexityColor = '#204b51';
  308. const cyanDarkPerplexityColor = '#203133';
  309.  
  310. const styles = `
  311. .textarea_wrapper {
  312. display: flex;
  313. flex-direction: column;
  314. }
  315.  
  316. @import url('https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600&display=swap');
  317.  
  318. .textarea_wrapper > textarea {
  319. width: 100%;
  320. background-color: rgba(0, 0, 0, 0.8);
  321. padding: 0 5px;
  322. border-radius: 0.5em;
  323. }
  324.  
  325. .textarea_label {
  326. }
  327.  
  328. .${helpTextCls} {
  329. background-color: #225;
  330. padding: 0.3em 0.7em;
  331. border-radius: 0.5em;
  332. margin: 1em 0;
  333. }
  334. .${helpTextCls} {
  335. cursor: text;
  336. }
  337.  
  338. .${helpTextCls} a {
  339. text-decoration: underline;
  340. }
  341. .${helpTextCls} a:hover {
  342. color: white;
  343. }
  344.  
  345. .${helpTextCls} code {
  346. font-size: 80%;
  347. background-color: rgba(255, 255, 255, 0.1);
  348. border-radius: 0.3em;
  349. padding: 0.1em;
  350. }
  351. .${helpTextCls} pre > code {
  352. background: none;
  353. }
  354. .${helpTextCls} pre {
  355. font-size: 80%;
  356. overflow: auto;
  357. background-color: rgba(255, 255, 255, 0.1);
  358. border-radius: 0.3em;
  359. padding: 0.1em 1em;
  360. }
  361. .${helpTextCls} li {
  362. list-style: circle;
  363. margin-left: 1em;
  364. }
  365. .${helpTextCls} hr {
  366. margin: 1em 0 0.5em 0;
  367. border-color: rgba(255, 255, 255, 0.1);
  368. }
  369.  
  370. .${helpTextCls} table {
  371. border: 1px solid rgba(255, 255, 255, 0.1);
  372. border-radius: 0.5em;
  373. display: inline-block;
  374. }
  375. .${helpTextCls} table td, .${helpTextCls} table th {
  376. padding: 0.1em 0.5em;
  377. }
  378.  
  379. .btn-helper {
  380. margin-left: 20px
  381. }
  382.  
  383. .modal {
  384. display: none;
  385. position: fixed;
  386. z-index: 1000;
  387. left: 0;
  388. top: 0;
  389. width: 100%;
  390. height: 100%;
  391. overflow: auto;
  392. background-color: rgba(0, 0, 0, 0.8)
  393. }
  394.  
  395. .modal-content {
  396. display: flex;
  397. margin: 1em auto;
  398. width: calc(100vw - 2em);
  399. padding: 20px;
  400. border: 1px solid #333;
  401. background: linear-gradient(135deg, #151517, #202025);
  402. border-radius: 6px;
  403. color: rgb(206, 206, 210);
  404. flex-direction: column;
  405. position: relative;
  406. overflow-y: auto;
  407. cursor: default;
  408. font-family: 'Fira Sans', sans-serif;
  409. }
  410.  
  411. .${modalTabGroupTabsCls} {
  412. display: flex;
  413. flex-direction: row;
  414. }
  415.  
  416. .modal-content .${modalTabGroupTabsCls} > button {
  417. border-radius: 0.5em 0.5em 0 0;
  418. border-bottom: 0;
  419. padding: 0.2em 0.5em 0 0.5em;
  420. background-color: #1e293b;
  421. color: rgba(255, 255, 255, 0.5);
  422. outline-bottom: none;
  423. white-space: nowrap;
  424. }
  425.  
  426. .modal-content .${modalTabGroupTabsCls} > button.${modalTabGroupActiveCls} {
  427. /* background-color: #3b82f6; */
  428. color: white;
  429. text-shadow: 0 0 1px currentColor;
  430. padding: 0.3em 0.5em 0.2em 0.5em;
  431. }
  432.  
  433. .modal-content .${modalTabGroupContentCls} {
  434. display: flex;
  435. flex-direction: column;
  436. gap: 1em;
  437. padding-top: 1em;
  438. }
  439.  
  440. .${modalSettingsTitleCls} {
  441. background: linear-gradient(to bottom, white, gray);
  442. -webkit-background-clip: text;
  443. background-clip: text;
  444. -webkit-text-fill-color: transparent;
  445. font-weight: bold;
  446. font-size: 3em;
  447. text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
  448. user-select: none;
  449. margin-top: -0.33em;
  450. margin-bottom: -0.33em;
  451. }
  452.  
  453. .${modalSettingsTitleCls} .animate-letter {
  454. display: inline-block;
  455. background: inherit;
  456. -webkit-background-clip: text;
  457. background-clip: text;
  458. -webkit-text-fill-color: transparent;
  459. transition: transform 0.3s ease-out;
  460. }
  461.  
  462. .${modalSettingsTitleCls} .animate-letter.active {
  463. /* Move and highlight on active */
  464. transform: translateY(-10px) rotate(5deg);
  465. -webkit-text-fill-color: #4dabff;
  466. text-shadow: 0 0 5px #4dabff, 0 0 10px #4dabff;
  467. }
  468.  
  469. .modal-content .hover\\:scale-110:hover {
  470. transform: scale(1.1);
  471. }
  472.  
  473. .modal-content label {
  474. padding-right: 10px;
  475. }
  476.  
  477. .modal-content hr {
  478. height: 1px;
  479. margin: 1em 0;
  480. border-color: rgba(255, 255, 255, 0.1);
  481. }
  482.  
  483. .modal-content hr.${modalTabGroupSeparatorCls} {
  484. margin: 0 -1em 0 -1em;
  485. }
  486.  
  487. .modal-content input[type="checkbox"] {
  488. appearance: none;
  489. width: 1.2em;
  490. height: 1.2em;
  491. border: 2px solid #ffffff80;
  492. border-radius: 0.25em;
  493. background-color: transparent;
  494. transition: all 0.2s ease;
  495. cursor: pointer;
  496. position: relative;
  497. }
  498.  
  499. .modal-content input[type="checkbox"]:checked {
  500. background-color: #3b82f6;
  501. border-color: #3b82f6;
  502. }
  503.  
  504. .modal-content input[type="checkbox"]:checked::after {
  505. content: '';
  506. position: absolute;
  507. left: 50%;
  508. top: 50%;
  509. width: 0.4em;
  510. height: 0.7em;
  511. border: solid white;
  512. border-width: 0 2px 2px 0;
  513. transform: translate(-50%, -60%) rotate(45deg);
  514. }
  515.  
  516. .modal-content input[type="checkbox"]:hover {
  517. border-color: #ffffff;
  518. }
  519.  
  520. .modal-content input[type="checkbox"]:focus {
  521. outline: 2px solid #3b82f680;
  522. outline-offset: 2px;
  523. }
  524.  
  525. .modal-content .checkbox_label {
  526. color: white;
  527. line-height: 1.5;
  528. }
  529.  
  530. .modal-content .checkbox_wrapper {
  531. display: flex;
  532. align-items: center;
  533. gap: 0.5em;
  534. }
  535.  
  536. .modal-content .number_label {
  537. margin-left: 0.5em;
  538. }
  539.  
  540. .modal-content .color_wrapper {
  541. display: flex;
  542. align-items: center;
  543. }
  544.  
  545. .modal-content .color_label {
  546. margin-left: 0.5em;
  547. }
  548.  
  549. .modal-content input, .modal-content button {
  550. background-color: #1e293b;
  551. border: 2px solid #ffffff80;
  552. border-radius: 0.5em;
  553. color: white;
  554. padding: 0.5em;
  555. transition: border-color 0.3s ease, outline 0.3s ease;
  556. }
  557.  
  558. .modal-content input:hover, .modal-content button:hover {
  559. border-color: #ffffff;
  560. }
  561.  
  562. .modal-content input:focus, .modal-content button:focus {
  563. outline: 2px solid #3b82f680;
  564. outline-offset: 2px;
  565. }
  566.  
  567. .modal-content input[type="number"] {
  568. padding: 0.5em;
  569. transition: border-color 0.3s ease, outline 0.3s ease;
  570. }
  571.  
  572. .modal-content input[type="color"] {
  573. padding: 0;
  574. height: 2em;
  575. }
  576.  
  577. .modal-content input[type="color"]:hover {
  578. border-color: #ffffff;
  579. }
  580.  
  581. .modal-content input[type="color"]:focus {
  582. outline: 2px solid #3b82f680;
  583. outline-offset: 2px;
  584. }
  585.  
  586. .modal-content h1 + hr {
  587. margin-top: 0.5em;
  588. }
  589.  
  590.  
  591. .modal-content select {
  592. appearance: none;
  593. background-color: #1e293b; /* Dark blue background */
  594. border: 2px solid #ffffff80;
  595. border-radius: 0.5em;
  596. padding: 0.3em 2em 0.3em 0.5em;
  597. color: white;
  598. font-size: 1em;
  599. cursor: pointer;
  600. transition: all 0.2s ease;
  601. 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");
  602. background-repeat: no-repeat;
  603. background-position: right 0.5em center;
  604. background-size: 1.2em;
  605. }
  606.  
  607. .modal-content select option {
  608. background-color: #1e293b; /* Match select background */
  609. color: white;
  610. padding: 0.5em;
  611. }
  612.  
  613. .modal-content select:hover {
  614. border-color: #ffffff;
  615. }
  616.  
  617. .modal-content select:focus {
  618. outline: 2px solid #3b82f680;
  619. outline-offset: 2px;
  620. }
  621.  
  622. .modal-content .select_label {
  623. color: white;
  624. margin-left: 0.5em;
  625. }
  626.  
  627. .modal-content .select_wrapper {
  628. display: flex;
  629. align-items: center;
  630. }
  631.  
  632. .close {
  633. color: rgb(206, 206, 210);
  634. float: right;
  635. font-size: 28px;
  636. font-weight: bold;
  637. position: absolute;
  638. right: 20px;
  639. top: 5px;
  640. }
  641.  
  642. .close:hover,
  643. .close:focus {
  644. color: white;
  645. text-decoration: none;
  646. cursor: pointer;
  647. }
  648.  
  649. #copied-modal,#copied-modal-2 {
  650. padding: 5px 5px;
  651. background:gray;
  652. position:absolute;
  653. display: none;
  654. color: white;
  655. font-size: 15px;
  656. }
  657.  
  658. label > div.select-none {
  659. user-select: text;
  660. cursor: initial;
  661. }
  662.  
  663. .${tagsContainerCls} {
  664. display: flex;
  665. flex-direction: row;
  666. margin: 5px 0;
  667. }
  668. .${tagsContainerCls}.${threadTagContainerCls} {
  669. margin-left: 0.5em;
  670. margin-right: 0.5em;
  671. margin-bottom: 2px;
  672. }
  673.  
  674. .${tagContainerCompactCls} {
  675. margin-top: -2em;
  676. margin-bottom: 1px;
  677. }
  678. .${tagContainerCompactCls} .${tagFenceCls} {
  679. margin: 0;
  680. padding: 1px;
  681. }
  682. .${tagContainerCompactCls} .${tagCls} {
  683. }
  684. .${tagContainerCompactCls} .${tagAllFencesWrapperCls} {
  685. gap: 1px;
  686. }
  687. .${tagContainerCompactCls} .${tagRestOfTagsWrapperCls} {
  688. margin: 1px;
  689. }
  690. .${tagContainerCompactCls} .${tagRestOfTagsWrapperCls},
  691. .${tagContainerCompactCls} .${tagFenceContentCls},
  692. .${tagContainerCompactCls} .${tagDirectoryContentCls} {
  693. gap: 1px;
  694. }
  695.  
  696. .${tagContainerWiderCls} {
  697. margin-left: -6em;
  698. margin-right: -6em;
  699. }
  700. .${tagContainerWiderCls} .${tagCls} {
  701. }
  702.  
  703. .${tagContainerWideCls} {
  704. margin-left: -12em;
  705. margin-right: -12em;
  706. }
  707.  
  708. .${tagContainerExtraWideCls} {
  709. margin-left: -16em;
  710. margin-right: -16em;
  711. max-width: 100vw;
  712. }
  713.  
  714. .${tagsContainerCls} {
  715. @media (max-width: 768px) {
  716. margin-left: 0 !important;
  717. margin-right: 0 !important;
  718. }
  719. }
  720.  
  721.  
  722. .${tagCls} {
  723. border: 1px solid #3b3b3b;
  724. background-color: #282828;
  725. /*color: rgba(255, 255, 255, 0.482);*/ /* equivalent of #909090; when on #282828 background */
  726. padding: 0px 8px 0 8px;
  727. border-radius: 4px;
  728. cursor: pointer;
  729. transition: background-color 0.2s, color 0.2s;
  730. display: inline-block;
  731. color: #E8E8E6;
  732. user-select: none;
  733. }
  734. .${tagCls}.${tagDarkTextCls} {
  735. color: #171719;
  736. }
  737. .${tagCls} span {
  738. display: inline-block;
  739. }
  740.  
  741. .${tagCls}.${tagTweakNoBorderCls} {
  742. border: none;
  743. }
  744.  
  745. .${tagCls}.${tagTweakSlimPaddingCls} {
  746. padding: 0px 4px 0 4px;
  747. }
  748.  
  749. .${tagCls} .${tagIconCls} {
  750. width: 16px;
  751. height: 16px;
  752. margin-right: 2px;
  753. margin-left: -4px;
  754. margin-top: -4px;
  755. vertical-align: middle;
  756. display: inline-block;
  757. filter: invert(1);
  758. }
  759. .${tagCls}.${tagDarkTextCls} .${tagIconCls} {
  760. filter: none;
  761. }
  762. .${tagCls}.${tagTweakSlimPaddingCls} .${tagIconCls} {
  763. margin-left: -2px;
  764. }
  765. .${tagCls} span {
  766. position: relative;
  767. top: 1.5px;
  768. }
  769. .${tagCls}.${tagTweakTextShadowCls} span {
  770. text-shadow: 1px 0 0.5px black, -1px 0 0.5px black, 0 1px 0.5px black, 0 -1px 0.5px black;
  771. }
  772. .${tagCls}.${tagTweakTextShadowCls}.${tagDarkTextCls} span {
  773. text-shadow: 1px 0 0.5px white, -1px 0 0.5px white, 0 1px 0.5px white, 0 -1px 0.5px white;
  774. }
  775. .${tagCls}:hover {
  776. background-color: #333;
  777. color: #fff;
  778. transform: scale(1.02);
  779. }
  780. .${tagCls}.${tagDarkTextCls}:hover {
  781. /* color: #171717; */
  782. color: #2f2f2f;
  783. }
  784. .${tagCls}:active {
  785. transform: scale(0.98);
  786. }
  787.  
  788. .${tagPaletteCls} {
  789. display: flex;
  790. flex-wrap: wrap;
  791. gap: 1px;
  792. }
  793. .${tagPaletteCls} .${tagPaletteItemCls} {
  794. text-shadow: 1px 0 1px black, -1px 0 1px black, 0 1px 1px black, 0 -1px 1px black;
  795. width: 40px;
  796. height: 25px;
  797. display: inline-block;
  798. text-align: center;
  799. padding: 0 2px;
  800. transition: color 0.2s, border 0.1s;
  801. border: 2px solid transparent;
  802. }
  803.  
  804. .${tagPaletteItemCls}:hover {
  805. cursor: pointer;
  806. color: white;
  807. border: 2px solid white;
  808. }
  809.  
  810. .${tagsPreviewCls} {
  811. background-color: #191a1a;
  812. padding: 0.5em 1em;
  813. border-radius: 1em;
  814. }
  815.  
  816. .${tagAllFencesWrapperCls} {
  817. display: flex;
  818. flex-direction: row;
  819. gap: 5px;
  820. }
  821.  
  822. .${tagRestOfTagsWrapperCls} {
  823. display: flex;
  824. flex-direction: row;
  825. flex-wrap: wrap;
  826. align-content: flex-start;
  827. gap: 5px;
  828. margin: 8px;
  829. }
  830.  
  831. .${tagFenceCls} {
  832. display: flex;
  833. margin: 5px 0;
  834. padding: 5px;
  835. border-radius: 4px;
  836. }
  837.  
  838. .${tagFenceContentCls} {
  839. display: flex;
  840. flex-direction: column;
  841. flex-wrap: wrap;
  842. gap: 5px;
  843. }
  844.  
  845. .${tagDirectoryCls} {
  846. position: relative;
  847. display: flex;
  848. z-index: 100;
  849. }
  850. .${tagDirectoryCls}:hover .${tagDirectoryContentCls} {
  851. display: flex;
  852. }
  853. .${tagDirectoryContentCls} {
  854. position: absolute;
  855. display: none;
  856. flex-direction: column;
  857. gap: 5px;
  858. top: 0px;
  859. padding-bottom: 1px;
  860. left: -5px;
  861. transform: translateY(-100%);
  862. background: rgba(0, 0, 0, 0.5);
  863. padding: 5px;
  864. border-radius: 4px;
  865. flex-wrap: nowrap;
  866. width: max-content;
  867. }
  868. .${tagDirectoryContentCls} .${tagCls} {
  869. white-space: nowrap;
  870. width: fit-content;
  871. }
  872.  
  873. .${queryBoxCls} {
  874. flex-wrap: wrap;
  875. }
  876.  
  877. .${controlsAreaCls} {
  878. grid-template-columns: repeat(4,minmax(0,1fr))
  879. }
  880.  
  881. .${textAreaCls} {
  882. grid-column-end: 5;
  883. }
  884.  
  885. .${standardButtonCls} {
  886. grid-column-start: 4;
  887. }
  888.  
  889. .${roundedMD} {
  890. border-radius: 0.375rem!important;
  891. }
  892.  
  893. #${leftSettingsButtonId} svg {
  894. transition: fill 0.2s;
  895. }
  896. #${leftSettingsButtonId}:hover svg {
  897. fill: #fff !important;
  898. }
  899.  
  900. .w-collapsedSideBarWidth #${leftSettingsButtonId} span {
  901. display: none;
  902. }
  903.  
  904. .w-collapsedSideBarWidth #${leftSettingsButtonId} {
  905. width: 100%;
  906. border-radius: 0.25rem;
  907. height: 40px;
  908. }
  909.  
  910. #${leftSettingsButtonWrapperId} {
  911. display: flex;
  912. padding: 0.1em 0.2em;
  913. justify-content: flex-start;
  914. }
  915.  
  916. .w-collapsedSideBarWidth #${leftSettingsButtonWrapperId} {
  917. justify-content: center;
  918. }
  919.  
  920. .${lucideIconParentCls} > img {
  921. transition: opacity 0.2s ease;
  922. }
  923.  
  924. .${lucideIconParentCls}:hover > img, a.dark\\:text-textMainDark .${lucideIconParentCls} > img {
  925. opacity: 1;
  926. }
  927.  
  928. .${leftPanelSlimCls} > .py-md {
  929. margin-left: -0.1em;
  930. }
  931.  
  932. .${leftPanelSlimCls} > .py-md > div.flex-col > * {
  933. /* background: red; */
  934. margin-right: 0;
  935. max-width: 40px;
  936. }
  937.  
  938. .${modelLabelCls} {
  939. color: #888;
  940. /* padding is from style attr */
  941. transition: color 0.2s, background-color 0.2s, border 0.2s;
  942. /*
  943. margin-right: 0.5em;
  944. margin-left: 0.5em;
  945. */
  946. padding-top: 3px;
  947. /*margin-right: 0.5em;*/
  948. }
  949. button.${modelIconButtonCls} {
  950. padding-right: 1.0em;
  951. padding-left: 1.0em;
  952. gap: 5px;
  953. }
  954. button:hover > .${modelLabelCls} {
  955. color: #fff;
  956. }
  957. button.${modelIconButtonCls} > .min-w-0 {
  958. min-width: 16px;
  959. margin-right: 0.0em;
  960. }
  961. button.${modelLabelRemoveCpuIconCls} {
  962. /* margin-left: 0.5em; */
  963. /* padding-left: 0.5em; */
  964. padding-right: 1.25em;
  965. }
  966. .${modelIconCls} {
  967. width: 16px;
  968. min-width: 16px;
  969. height: 16px;
  970. margin-right: 2px;
  971. margin-left: 0;
  972. margin-top: -0px;
  973. opacity: 0.5;
  974. transition: opacity 0.2s;
  975. }
  976. button.${modelLabelLargerIconsCls} .${modelIconCls} {
  977. transform: scale(1.2);
  978. }
  979. button:hover .${modelIconCls} {
  980. opacity: 1;
  981. }
  982. button.${modelLabelRemoveCpuIconCls} .${modelLabelCls} {
  983. /*margin-right: 0.5em; */
  984. }
  985. button.${modelLabelRemoveCpuIconCls}:has(.${reasoningModelCls}) .${modelLabelCls} {
  986. /*margin-right: 0.5em; */
  987. }
  988. button.${modelLabelRemoveCpuIconCls}.${notReasoningModelCls} .${modelLabelCls} {
  989. /* margin-right: 0.0em; */
  990. }
  991. .${modelLabelRemoveCpuIconCls} div:has(div > svg.tabler-icon-cpu) {
  992. display: none;
  993. }
  994.  
  995. button:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}) {
  996. border: 1px solid #333;
  997. }
  998. button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}) {
  999. background: #333 !important;
  1000. }
  1001. /* Apply style even if the span is empty */
  1002. button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}:empty) {
  1003. border: 1px solid #333;
  1004. }
  1005. button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}:empty):hover {
  1006. background: #333 !important;
  1007. }
  1008.  
  1009. .${modelLabelCls}.${modelLabelStyleButtonWhiteCls} {
  1010. color: #8D9191 !important;
  1011. }
  1012. button:hover > .${modelLabelCls}.${modelLabelStyleButtonWhiteCls} {
  1013. color: #fff !important;
  1014. }
  1015. .${modelIconButtonCls} svg[stroke] {
  1016. stroke: #8D9191 !important;
  1017. }
  1018. .${modelIconButtonCls}:hover svg[stroke] {
  1019. stroke: #fff !important;
  1020. }
  1021. button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}) {
  1022. background: #191A1A !important;
  1023. color: #2D2F2F !important;
  1024. }
  1025. button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}):hover {
  1026. color: #8D9191 !important;
  1027. }
  1028. /* Apply style even if the span is empty */
  1029. button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}:empty) {
  1030. background: #191A1A !important;
  1031. color: #2D2F2F !important;
  1032. }
  1033. button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}:empty):hover {
  1034. color: #8D9191 !important;
  1035. }
  1036.  
  1037. .${modelLabelCls}.${modelLabelStyleButtonCyanCls} {
  1038. color: ${cyanPerplexityColor};
  1039. }
  1040. button:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) {
  1041. border: 1px solid ${cyanMediumPerplexityColor};
  1042. background: ${cyanDarkPerplexityColor} !important;
  1043. }
  1044. button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) {
  1045. border: 1px solid ${cyanPerplexityColor};
  1046. }
  1047. /* Apply style even if the span is empty */
  1048. button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}:empty) {
  1049. border: 1px solid ${cyanMediumPerplexityColor};
  1050. background: ${cyanDarkPerplexityColor} !important;
  1051. }
  1052. button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}:empty):hover {
  1053. border: 1px solid ${cyanPerplexityColor};
  1054. }
  1055. .${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) svg[stroke] {
  1056. stroke: ${cyanPerplexityColor} !important;
  1057. }
  1058. .${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}):hover svg[stroke] {
  1059. stroke: #fff !important;
  1060. }
  1061.  
  1062. button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}) {
  1063. color: #888 !important;
  1064. }
  1065. button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}):hover {
  1066. color: #fff !important;
  1067. }
  1068.  
  1069. .${reasoningModelCls} {
  1070. width: 16px;
  1071. height: 16px;
  1072. /*
  1073. margin-right: 2px;
  1074. margin-left: 2px;
  1075. */
  1076. margin-top: -2px;
  1077. filter: invert();
  1078. opacity: 0.5;
  1079. transition: opacity 0.2s;
  1080. }
  1081. button.${modelLabelLargerIconsCls} .${reasoningModelCls} {
  1082. transform: scale(1.2);
  1083. }
  1084. button:hover .${reasoningModelCls} {
  1085. opacity: 1;
  1086. }
  1087.  
  1088. .${errorIconCls} {
  1089. width: 16px;
  1090. height: 16px;
  1091. margin-right: 4px;
  1092. margin-left: 4px;
  1093. margin-top: -0px;
  1094. opacity: 0.75;
  1095. transition: opacity 0.2s;
  1096. }
  1097. button.${modelLabelLargerIconsCls} .${errorIconCls} {
  1098. transform: scale(1.2);
  1099. }
  1100. button:hover .${errorIconCls} {
  1101. opacity: 1;
  1102. }
  1103. /* button:has(.${reasoningModelCls}) > div > div > svg {
  1104. width: 32px;
  1105. height: 16px;
  1106. margin-left: 8px;
  1107. margin-right: 12px;
  1108. margin-top: 0px;
  1109. min-width: 16px;
  1110. background-color: cyan;
  1111. }
  1112. button:has(.${reasoningModelCls}) > div > div:has(svg) {
  1113. width: 16px;
  1114. height: 16px;
  1115. min-width: 30px;
  1116. background-color: purple;
  1117. } */
  1118.  
  1119.  
  1120. .${iconColorCyanCls} {
  1121. filter: invert(54%) sepia(84%) saturate(431%) hue-rotate(139deg) brightness(97%) contrast(90%);
  1122. transition: filter 0.2s;
  1123. }
  1124.  
  1125. button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) .${iconColorCyanCls} {
  1126. filter: invert(100%);
  1127. }
  1128.  
  1129. .${iconColorGrayCls} {
  1130. filter: invert(100%);
  1131. opacity: 0.5;
  1132. transition: filter 0.2s;
  1133. }
  1134. button:has(.${reasoningModelCls}):hover .${iconColorGrayCls} {
  1135. filter: invert(100%);
  1136. }
  1137.  
  1138. .${iconColorWhiteCls} {
  1139. filter: invert(50%);
  1140. transition: filter 0.2s;
  1141. }
  1142. button:has(.${reasoningModelCls}):hover .${iconColorWhiteCls} {
  1143. filter: invert(100%);
  1144. }
  1145.  
  1146.  
  1147. .${sideMenuHiddenCls} {
  1148. display: none;
  1149. }
  1150.  
  1151. .${sideMenuLabelsHiddenCls} .p-sm > div.font-sans.text-xs {
  1152. display: none;
  1153. }
  1154.  
  1155.  
  1156.  
  1157. .${enhancedSubmitButtonCls} {
  1158. position: absolute;
  1159. top: 0;
  1160. left: 0;
  1161. width: 101%;
  1162. height: 101%;
  1163. border-radius: inherit;
  1164. cursor: pointer;
  1165. background: transparent;
  1166. box-shadow: 0 0 0 1px transparent;
  1167. z-index: 10;
  1168. display: flex;
  1169. align-items: center;
  1170. justify-content: center;
  1171. opacity: 0;
  1172. transform: scale(1.1);
  1173. transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  1174. overflow: visible;
  1175. pointer-events: none;
  1176. }
  1177.  
  1178. /* ISSUE: Using hard-coded 'active' class here instead of enhancedSubmitButtonActiveCls */
  1179. .${enhancedSubmitButtonCls}.active {
  1180. opacity: 0.5;
  1181. transform: scale(1);
  1182. pointer-events: auto;
  1183. box-shadow: 0 0 0 1px cyan inset;
  1184. }
  1185.  
  1186. .${enhancedSubmitButtonCls}:hover {
  1187. opacity: 1;
  1188. background: radial-gradient(circle at right top, rgb(23, 8, 56), rgb(4, 2, 12));
  1189. }
  1190.  
  1191. .${enhancedSubmitButtonCls}::before {
  1192. content: '';
  1193. position: absolute;
  1194. top: -2px;
  1195. left: -2px;
  1196. right: -2px;
  1197. bottom: -2px;
  1198. background: transparent;
  1199. z-index: -1;
  1200. box-shadow: 0 0 0 1.2px transparent;
  1201. border-radius: inherit;
  1202. transition: opacity 0.4s ease-in-out, box-shadow 0.4s ease-in-out;
  1203. opacity: 0;
  1204. }
  1205.  
  1206. /* ISSUE: Using hard-coded 'active' class here instead of enhancedSubmitButtonActiveCls */
  1207. .${enhancedSubmitButtonCls}.active::before {
  1208. opacity: 0.9;
  1209. box-shadow: 0 0 0 1.2px #00ffff;
  1210. animation: gradientBorder 3s ease infinite;
  1211. }
  1212.  
  1213. .${enhancedSubmitButtonCls}:hover::before {
  1214. opacity: 1;
  1215. }
  1216.  
  1217. @keyframes gradientBorder {
  1218. 0% { box-shadow: 0 0 0 1.2px rgba(0, 255, 255, 0.6); }
  1219. 50% { box-shadow: 0 0 0 1.2px rgba(0, 255, 255, 1), 0 0 8px rgba(0, 255, 255, 0.6); }
  1220. 100% { box-shadow: 0 0 0 1.2px rgba(0, 255, 255, 0.6); }
  1221. }
  1222.  
  1223. @keyframes pulseIndicator {
  1224. 0% { transform: scale(1); opacity: 0.6; }
  1225. 50% { transform: scale(1.5); opacity: 1; }
  1226. 100% { transform: scale(1); opacity: 0.6; }
  1227. }
  1228.  
  1229. .${enhancedSubmitButtonPhTextCls} {
  1230. font-family: 'JetBrains Mono', monospace;
  1231. color: #00c1ff;
  1232. display: none;
  1233. position: absolute;
  1234. font-size: 20px;
  1235. user-select: none;
  1236. align-items: center;
  1237. justify-content: center;
  1238. width: 100%;
  1239. height: 100%;
  1240. }
  1241.  
  1242. .${enhancedSubmitButtonCls}:hover .${enhancedSubmitButtonPhTextCls} {
  1243. display: flex;
  1244. }
  1245.  
  1246. /* Prompt area with active toggle tags */
  1247. textarea.${promptAreaKeyListenerCls} {
  1248. box-shadow: 0 0 0 1px rgba(31, 184, 205, 0.2), 0 0px 0px rgba(31, 184, 205, 0);
  1249. transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  1250. border-color: rgba(31, 184, 205, 0.2);
  1251. position: relative;
  1252. background-image: linear-gradient(to bottom, rgba(31, 184, 205, 0.03), transparent);
  1253. }
  1254.  
  1255. /* Nice glow effect when focused */
  1256. textarea.${promptAreaKeyListenerCls}:focus {
  1257. box-shadow: 0 0 0 1px rgba(31, 184, 205, 0.5), 0 0 8px 1px rgba(31, 184, 205, 0.3);
  1258. border-color: rgba(31, 184, 205, 0.5);
  1259. background-image: linear-gradient(to bottom, rgba(31, 184, 205, 0.05), transparent);
  1260. }
  1261.  
  1262. /* Active indicator for textarea */
  1263. .${promptAreaKeyListenerIndicatorCls} {
  1264. position: absolute;
  1265. bottom: 5px;
  1266. right: 5px;
  1267. width: 4px;
  1268. height: 4px;
  1269. border-radius: 50%;
  1270. background-color: rgba(31, 184, 205, 0.6);
  1271. z-index: 5;
  1272. pointer-events: none;
  1273. box-shadow: 0 0 4px 1px rgba(31, 184, 205, 0.4);
  1274. animation: pulseIndicator 2s ease-in-out infinite;
  1275. opacity: 0;
  1276. transform: scale(0);
  1277. transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
  1278. }
  1279.  
  1280. /* When actually visible, override initial zero values */
  1281. .${promptAreaKeyListenerIndicatorCls}.visible {
  1282. opacity: 1;
  1283. transform: scale(1);
  1284. }
  1285.  
  1286. /* Pulse focus effect when Enter is pressed */
  1287. textarea.${pulseFocusCls} {
  1288. box-shadow: 0 0 0 2px rgba(31, 184, 205, 0.8), 0 0 12px 4px rgba(31, 184, 205, 0.6) !important;
  1289. border-color: rgba(31, 184, 205, 0.8) !important;
  1290. transition: none !important;
  1291. }
  1292.  
  1293.  
  1294. `;
  1295.  
  1296.  
  1297. const TAG_POSITION = {
  1298. BEFORE: 'before',
  1299. AFTER: 'after',
  1300. CARET: 'caret',
  1301. WRAP: 'wrap',
  1302. };
  1303.  
  1304. const TAG_CONTAINER_TYPE = {
  1305. NEW: 'new',
  1306. NEW_IN_COLLECTION: 'new-in-collection',
  1307. THREAD: 'thread',
  1308. ALL: 'all',
  1309. };
  1310.  
  1311. const tagsHelpText = `
  1312. Each line is one tag.
  1313. Non-field text is what will be inserted into prompt.
  1314. Field is denoted by \`<\` and \`>\`, field name is before \`:\`, field value after \`:\`.
  1315.  
  1316. Supported fields:
  1317. - \`label\`: tag label shown on tag "box" (new items around prompt input area)
  1318. - \`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)
  1319. - \`color\`: tag color; CSS colors supported, you can use colors from a pre-generated palette via \`%\` syntax, e.g. \`<color:%5>\`. See palette bellow.
  1320. - \`tooltip\`: shown on hover (aka title); (default) tooltip can be disabled when this field is set to empty string - \`<tooltip:>\`
  1321. - \`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)
  1322. - \`hide\`: hide the tag from the tag list
  1323. - \`link\`: link to a URL, e.g. \`<link:https://example.com>\`, can be used for collections. only one link per tag is supported.
  1324. - \`link-target\`: target of the link, e.g. \`<link-target:_blank>\` (opens in new tab), default is \`_self\` (same tab).
  1325. - \`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.
  1326. - \`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>\`.
  1327. - \`set-mode\`: set the query mode: \`pro\` or \`research\`, e.g. \`<set-mode:pro>\`
  1328. - \`set-model\`: set the model, e.g. \`<set-model:claude-3-7-sonnet-thinking>\`
  1329. - \`set-sources\`: set the sources, e.g. \`<set-sources:001>\` for disabled first source (web), disabled second source (academic), enabled third source (social)
  1330. - \`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>\`
  1331. - \`dir\`: unique identifier for a directory tag (it will not insert text into prompt)
  1332. - \`in-dir\`: identifier of the parent directory this tag belongs to
  1333. - \`fence\`: unique identifier for a fence definition (hidden by default)
  1334. - \`in-fence\`: identifier of the fence this tag belongs to
  1335. - \`fence-width\`: CSS width for a fence, e.g. \`<fence-width:10em>\`
  1336. - \`fence-border-style\`: CSS border style for a fence (e.g., solid, dashed, dotted)
  1337. - \`fence-border-color\`: CSS color or a palette \`%\` syntax for a fence border
  1338. - \`fence-border-width\`: CSS width for a fence border
  1339.  
  1340. ---
  1341.  
  1342. | String | Replacement | Example |
  1343. |---|---|---|
  1344. | \`\\n\` | newline | |
  1345. | \`$$time$$\` | current time | \`23:05\` |
  1346. | \`$$wrap$$\` | sets position where existing text will be inserted | |
  1347.  
  1348. ---
  1349.  
  1350. Examples:
  1351. \`\`\`
  1352. stable diffusion web ui - <label:SDWU>
  1353. , prefer concise modern syntax and style, <position:caret><label:concise modern>
  1354. tell me a joke<label:Joke><tooltip:>
  1355. tell me a joke<label:Joke & Submit><auto-submit>
  1356. <label:Sonnet><toggle-mode><set-model:claude-3-7-sonnet-thinking><icon:brain>
  1357. <toggle-mode><label:Add Note><position:after><color:%2>\n\nNOTE: This is a toggle-mode note appended to the end of prompt
  1358. \`\`\`
  1359.  
  1360. Directory example:
  1361. \`\`\`
  1362. <dir:games>Games<icon:gamepad-2>
  1363. <in-dir:games>FFXIV: <color:%15><label:FFXIV>
  1364. <in-dir:games>Vintage Story - <label:VS>
  1365. \`\`\`
  1366.  
  1367. Fence example:
  1368. \`\`\`
  1369. <fence:anime><fence-border-style:dashed><fence-border-color:%10>
  1370. <in-fence:anime>Shounen
  1371. <in-fence:anime>Seinen
  1372. <in-fence:anime>Shoujo
  1373. \`\`\`
  1374.  
  1375. Another fence example:
  1376. \`\`\`
  1377. <fence:programming><fence-border-style:solid><fence-border-color:%20>
  1378. <in-fence:programming>Haskell
  1379. <in-fence:programming>Raku<label:🦋>
  1380. \`\`\`
  1381. `.trim();
  1382.  
  1383. const defaultTagColor = '#282828';
  1384.  
  1385. const changeValueUsingEvent = (selector, value) => {
  1386. debugLog('changeValueUsingEvent', value, selector);
  1387.  
  1388. const nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  1389. nativeTextareaValueSetter.call(selector, value);
  1390. const inputEvent = new Event('input', { bubbles: true });
  1391. selector.dispatchEvent(inputEvent);
  1392. };
  1393.  
  1394. const TAGS_PALETTE_COLORS_NUM = 16;
  1395. const TAGS_PALETTE_CLASSIC = Object.freeze((() => {
  1396. const step = 360 / TAGS_PALETTE_COLORS_NUM;
  1397. const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  1398. return _.flow(
  1399. _.map(x => startH + x * step, _),
  1400. _.map(h => color2k.hsla(h, startS, startL, startA), _),
  1401. _.sortBy(x => color2k.parseToHsla(x)[0], _)
  1402. )(_.range(0, TAGS_PALETTE_COLORS_NUM));
  1403. })());
  1404.  
  1405. const TAGS_PALETTE_PASTEL = Object.freeze((() => {
  1406. const step = 360 / TAGS_PALETTE_COLORS_NUM;
  1407. const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  1408. return _.flow(
  1409. _.map(x => startH + x * step, _),
  1410. _.map(h => color2k.hsla(h, startS - 0.2, startL + 0.2, startA), _),
  1411. _.sortBy(x => color2k.parseToHsla(x)[0], _)
  1412. )(_.range(0, TAGS_PALETTE_COLORS_NUM));
  1413. })());
  1414.  
  1415. const TAGS_PALETTE_GRIM = Object.freeze((() => {
  1416. const step = 360 / TAGS_PALETTE_COLORS_NUM;
  1417. const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  1418. return _.flow(
  1419. _.map(x => startH + x * step, _),
  1420. _.map(h => color2k.hsla(h, startS - 0.6, startL - 0.3, startA), _),
  1421. _.sortBy(x => color2k.parseToHsla(x)[0], _)
  1422. )(_.range(0, TAGS_PALETTE_COLORS_NUM));
  1423. })());
  1424.  
  1425. const TAGS_PALETTE_DARK = Object.freeze((() => {
  1426. const step = 360 / TAGS_PALETTE_COLORS_NUM;
  1427. const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  1428. return _.flow(
  1429. _.map(x => startH + x * step, _),
  1430. _.map(h => color2k.hsla(h, startS, startL - 0.4, startA), _),
  1431. _.sortBy(x => color2k.parseToHsla(x)[0], _)
  1432. )(_.range(0, TAGS_PALETTE_COLORS_NUM));
  1433. })());
  1434.  
  1435. const TAGS_PALETTE_GRAY = Object.freeze((() => {
  1436. const step = 1 / TAGS_PALETTE_COLORS_NUM;
  1437. return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(0, 0, step * x, 1));
  1438. })());
  1439.  
  1440. const TAGS_PALETTE_CYAN = Object.freeze((() => {
  1441. const step = 1 / TAGS_PALETTE_COLORS_NUM;
  1442. const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  1443. return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(startH, startS, step * x, 1));
  1444. })());
  1445.  
  1446. const TAGS_PALETTE_TRANSPARENT = Object.freeze((() => {
  1447. const step = 1 / TAGS_PALETTE_COLORS_NUM;
  1448. return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(0, 0, 0, step * x));
  1449. })());
  1450.  
  1451. const TAGS_PALETTE_HACKER = Object.freeze((() => {
  1452. const step = 1 / TAGS_PALETTE_COLORS_NUM;
  1453. return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(120, step * x, step * x * 0.5, 1));
  1454. })());
  1455.  
  1456. const TAGS_PALETTES = Object.freeze({
  1457. CLASSIC: TAGS_PALETTE_CLASSIC,
  1458. PASTEL: TAGS_PALETTE_PASTEL,
  1459. GRIM: TAGS_PALETTE_GRIM,
  1460. DARK: TAGS_PALETTE_DARK,
  1461. GRAY: TAGS_PALETTE_GRAY,
  1462. CYAN: TAGS_PALETTE_CYAN,
  1463. TRANSPARENT: TAGS_PALETTE_TRANSPARENT,
  1464. HACKER: TAGS_PALETTE_HACKER,
  1465. CUSTOM: 'CUSTOM',
  1466. });
  1467.  
  1468. const convertColorInPaletteFormat = currentPalette => value => currentPalette[parseInt(dropStr(1)(value), 10)] ?? defaultTagColor;
  1469.  
  1470. const TAG_HOME_PAGE_LAYOUT = {
  1471. DEFAULT: 'default',
  1472. COMPACT: 'compact',
  1473. WIDER: 'wider',
  1474. WIDE: 'wide',
  1475. EXTRA_WIDE: 'extra-wide',
  1476. };
  1477.  
  1478. const parseBinaryState = binaryStr => {
  1479. if (!/^[01-]+$/.test(binaryStr)) {
  1480. throw new Error('Invalid binary state: ' + binaryStr);
  1481. }
  1482. return binaryStr.split('').map(bit => bit === '1' ? true : bit === '0' ? false : null);
  1483. };
  1484.  
  1485. const processTagField = currentPalette => name => value => {
  1486. if (name === 'color' && value.startsWith('%')) return convertColorInPaletteFormat(currentPalette)(value);
  1487. if (name === 'hide') return true;
  1488. if (name === 'auto-submit') return true;
  1489. if (name === 'toggle-mode') return true;
  1490. if (name === 'set-sources') return parseBinaryState(value);
  1491. return value;
  1492. };
  1493.  
  1494. 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;
  1495. const parseOneTagLine = currentPalette => line =>
  1496. Array.from(line.matchAll(tagLineRegex)).reduce(
  1497. (acc, match) => {
  1498. const [fullMatch, field, value] = match;
  1499. const processedValue = processTagField(currentPalette)(field)(value);
  1500. return {
  1501. ...acc,
  1502. [_.camelCase(field)]: processedValue,
  1503. text: acc.text.replace(fullMatch, '').replace(/\\n/g, '\n'),
  1504. };
  1505. },
  1506. {
  1507. text: line,
  1508. color: defaultTagColor,
  1509. target: TAG_CONTAINER_TYPE.NEW,
  1510. hide: false,
  1511. 'link-target': '_self',
  1512. }
  1513. );
  1514.  
  1515. const parseTagsText = text => {
  1516. const lines = text.split('\n').filter(tag => tag.trim().length > 0);
  1517. const palette = getPalette(loadConfig()?.tagPalette);
  1518. return lines.map(parseOneTagLine(palette)).map((x, i) => ({ ...x, originalIndex: i }));
  1519. };
  1520.  
  1521. const getTagsContainer = () => $c(tagsContainerCls);
  1522.  
  1523. const posFromTag = tag => Object.values(TAG_POSITION).includes(tag.position) ? tag.position : TAG_POSITION.BEFORE;
  1524.  
  1525. const splitTextAroundWrap = (text) => {
  1526. const parts = text.split('$$wrap$$');
  1527. return {
  1528. before: parts[0] || '',
  1529. after: parts[1] || '',
  1530. };
  1531. };
  1532.  
  1533. const applyTagToString = (tag, val, caretPos) => {
  1534. const { text } = tag;
  1535. const timeString = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
  1536. const textAfterTime = text.replace(/\$\$time\$\$/g, timeString);
  1537. const { before: processedTextBefore, after: processedTextAfter } = splitTextAroundWrap(textAfterTime);
  1538. const processedText = processedTextBefore;
  1539.  
  1540. switch (posFromTag(tag)) {
  1541. case TAG_POSITION.BEFORE:
  1542. return `${processedText}${val}`;
  1543. case TAG_POSITION.AFTER:
  1544. return `${val}${processedText}`;
  1545. case TAG_POSITION.CARET:
  1546. return `${takeStr(caretPos)(val)}${processedText}${dropStr(caretPos)(val)}`;
  1547. case TAG_POSITION.WRAP:
  1548. return `${processedTextBefore}${val}${processedTextAfter}`;
  1549. default:
  1550. throw new Error(`Invalid position: ${tag.position}`);
  1551. }
  1552. };
  1553.  
  1554. const getPromptAreaFromTagsContainer = tagsContainerEl => PP.getAnyPromptArea(tagsContainerEl.parent());
  1555.  
  1556. const getPalette = paletteName => {
  1557. // Add this check for 'CUSTOM'
  1558. if (paletteName === TAGS_PALETTES.CUSTOM) {
  1559. // Use tagPaletteCustom from config or default if not found
  1560. return loadConfigOrDefault()?.tagPaletteCustom ?? defaultConfig.tagPaletteCustom;
  1561. }
  1562. // Fallback to predefined palettes or CLASSIC as default
  1563. const palette = TAGS_PALETTES[paletteName];
  1564. // Check if palette is an array before returning, otherwise return default
  1565. return Array.isArray(palette) ? palette : TAGS_PALETTES.CLASSIC;
  1566. };
  1567.  
  1568. // Function to update a toggle tag's visual state
  1569. const updateToggleTagState = (tagEl, tag, newToggleState) => {
  1570. if (!tagEl || !tag) return;
  1571. const isTagLight = color2k.getLuminance(tag.color) > loadConfigOrDefault().tagLuminanceThreshold;
  1572. const colorMod = isTagLight ? color2k.darken : color2k.lighten;
  1573. const hoverBgColor = color2k.toRgba(colorMod(tag.color, 0.1));
  1574. // For toggle tags, adjust the color based on toggle state
  1575. const toggledColor = newToggleState ? color2k.lighten(tag.color, 0.3) : tag.color;
  1576. // Update the tag element
  1577. tagEl.attr('data-toggled', newToggleState);
  1578. tagEl.css('background-color', toggledColor);
  1579. tagEl.attr('data-hoverBgColor', color2k.toHex(hoverBgColor));
  1580. // Update tooltip if using default
  1581. if (!tag.tooltip) {
  1582. const newTooltip = `${logPrefix} Toggle ${newToggleState ? 'off' : 'on'} - ${tag.label || 'tag'}`;
  1583. tagEl.prop('title', newTooltip);
  1584. }
  1585. };
  1586.  
  1587. const createTag = containerEl => isPreview => tag => {
  1588. if (tag.hide) return null;
  1589.  
  1590. // Generate a unique identifier for this toggle tag
  1591. const tagId = generateToggleTagId(tag);
  1592.  
  1593. // Get saved toggle state if this is a toggle-mode tag and tagToggleSave is enabled
  1594. const config = loadConfigOrDefault();
  1595. // Make sure tagToggledStates exists to prevent errors
  1596. if (!config.tagToggledStates) {
  1597. config.tagToggledStates = {};
  1598. saveConfig(config);
  1599. }
  1600. // TODO: rewrite most of code with _phTagToggleState - new util functions/classes for working with it
  1601. // Check both the in-memory toggle state and the saved toggle state (if tagToggleSave is enabled)
  1602. // In-memory toggle state takes precedence during the current session
  1603. const inMemoryToggleState = window._phTagToggleState && tagId ? window._phTagToggleState[tagId] : undefined;
  1604. const savedToggleState = (tagId && config.tagToggleSave) ? config.tagToggledStates[tagId] || false : false;
  1605. const isToggled = inMemoryToggleState !== undefined ? inMemoryToggleState : savedToggleState;
  1606.  
  1607. const labelString = tag.label ?? tag.text;
  1608. const isTagLight = color2k.getLuminance(tag.color) > loadConfig().tagLuminanceThreshold;
  1609. const colorMod = isTagLight ? color2k.darken : color2k.lighten;
  1610. const hoverBgColor = color2k.toRgba(colorMod(tag.color, 0.1));
  1611. const borderColor = color2k.toRgba(colorMod(tag.color, loadConfig().tagTweakRichBorderColor ? 0.2 : 0.1));
  1612.  
  1613. const clickHandler = async (evt) => {
  1614. debugLog('clicked', tag, evt);
  1615. if (tag.link) return;
  1616.  
  1617. // Handle toggle mode
  1618. if (tag.toggleMode) {
  1619. const el = jq(evt.currentTarget);
  1620. // Get the current toggle state directly from the element
  1621. // This is critical for handling multiple clicks correctly
  1622. const currentToggleState = el.attr('data-toggled') === 'true';
  1623. const newToggleState = !currentToggleState;
  1624.  
  1625. // Update the toggle state in config only if tagToggleSave is enabled
  1626. // Make sure tagId is valid before using it
  1627. if (tagId) {
  1628. const config = loadConfigOrDefault();
  1629. // Create a temporary in-memory toggle state for visual indication
  1630. // We'll track this regardless of tagToggleSave setting
  1631. window._phTagToggleState = window._phTagToggleState || {};
  1632. window._phTagToggleState[tagId] = newToggleState;
  1633. // Only save the toggle state permanently if the tagToggleSave setting is enabled
  1634. if (config.tagToggleSave) {
  1635. const updatedConfig = {
  1636. ...config,
  1637. tagToggledStates: {
  1638. ...config.tagToggledStates,
  1639. [tagId]: newToggleState
  1640. }
  1641. };
  1642. saveConfig(updatedConfig);
  1643. }
  1644.  
  1645. // Update visual indicators for submit buttons
  1646. updateToggleIndicators();
  1647. // Update the tag's visual state
  1648. updateToggleTagState(el, tag, newToggleState);
  1649. } else {
  1650. debugLog('Error: Invalid toggle tag ID', tag);
  1651. }
  1652. return;
  1653. }
  1654.  
  1655. // Regular tag handling for non-toggle tags
  1656. try {
  1657. // Apply all tag's actions and wait for them to complete
  1658. await applyTagActions(tag);
  1659.  
  1660. // Handle auto submit for this tag after all actions are applied
  1661. if (tag.autoSubmit) {
  1662. const submitButton = PP.submitButtonAny();
  1663. debugLogTags('[createTag] clickHandler: submitButton=', submitButton);
  1664. if (submitButton.length) {
  1665. if (submitButton.length > 1) {
  1666. debugLogTags('[createTag] clickHandler: multiple submit buttons found, using first one');
  1667. }
  1668. submitButton.first().click();
  1669. } else {
  1670. debugLogTags('[createTag] clickHandler: no submit button found');
  1671. }
  1672. } else {
  1673. // Focus the prompt area if we're not auto-submitting
  1674. const el = jq(evt.currentTarget);
  1675. const tagsContainer = el.closest(`.${tagsContainerCls}`);
  1676. if (tagsContainer.length) {
  1677. const promptArea = getPromptAreaFromTagsContainer(tagsContainer);
  1678. if (promptArea.length) {
  1679. promptArea[0].focus();
  1680. }
  1681. }
  1682. }
  1683. } catch (error) {
  1684. debugLog('Error applying tag actions:', error);
  1685. }
  1686. };
  1687.  
  1688. const tagFont = loadConfig().tagFont;
  1689.  
  1690. // Create tooltip message based on tag type - without using let
  1691. const tooltipMsg = tag.link
  1692. ? `${logPrefix} Open link: ${tag.link}`
  1693. : tag.toggleMode
  1694. ? `${logPrefix} Toggle ${isToggled ? 'off' : 'on'} - ${tag.label || 'tag'}`
  1695. : `${logPrefix} Insert \`${tag.text}\` at position \`${posFromTag(tag)}\``;
  1696.  
  1697. const defaultTooltip = tooltipMsg;
  1698.  
  1699. // For toggle tags, adjust the color based on toggle state
  1700. const toggledColor = isToggled ? color2k.lighten(tag.color, 0.3) : tag.color;
  1701. const backgroundColor = tag.toggleMode ? toggledColor : tag.color;
  1702.  
  1703. const tagEl = jq(`<div/>`)
  1704. .addClass(tagCls)
  1705. .prop('title', tag.tooltip ?? defaultTooltip)
  1706. .attr('data-tag', JSON.stringify(tag))
  1707. .css({
  1708. backgroundColor,
  1709. borderColor,
  1710. fontFamily: tagFont,
  1711. borderRadius: `${loadConfig().tagRoundness}px`,
  1712. })
  1713. .attr('data-color', color2k.toHex(tag.color))
  1714. .attr('data-hoverBgColor', color2k.toHex(hoverBgColor))
  1715. .attr('data-font', tagFont)
  1716. .attr('data-toggled', isToggled.toString())
  1717. .on('mouseenter', event => {
  1718. jq(event.currentTarget).css('background-color', hoverBgColor);
  1719. })
  1720. .on('mouseleave', event => {
  1721. const el = jq(event.currentTarget);
  1722. const isCurrentToggled = el.attr('data-toggled') === 'true';
  1723. const currentColor = tag.toggleMode && isCurrentToggled ?
  1724. color2k.lighten(tag.color, 0.3) : tag.color;
  1725. el.css('background-color', currentColor);
  1726. });
  1727.  
  1728. if (isTagLight) {
  1729. tagEl.addClass(tagDarkTextCls);
  1730. }
  1731.  
  1732. if (loadConfig()?.tagTweakNoBorder) {
  1733. tagEl.addClass(tagTweakNoBorderCls);
  1734. }
  1735. if (loadConfig()?.tagTweakSlimPadding) {
  1736. tagEl.addClass(tagTweakSlimPaddingCls);
  1737. }
  1738. if (loadConfig()?.tagTweakTextShadow) {
  1739. tagEl.addClass(tagTweakTextShadowCls);
  1740. }
  1741.  
  1742. const textEl = jq('<span/>')
  1743. .text(labelString)
  1744. .css({
  1745. 'font-weight': loadConfig().tagBold ? 'bold' : 'normal',
  1746. 'font-style': loadConfig().tagItalic ? 'italic' : 'normal',
  1747. 'font-size': `${loadConfig().tagFontSize}px`,
  1748. 'transform': `translateY(${loadConfig().tagTextYOffset}px)`,
  1749. });
  1750.  
  1751. if (tag.icon) {
  1752. const iconEl = jq('<img/>')
  1753. .attr('src', getIconUrl(tag.icon))
  1754. .addClass(tagIconCls)
  1755. .css({
  1756. 'width': `${loadConfig().tagIconSize}px`,
  1757. 'height': `${loadConfig().tagIconSize}px`,
  1758. 'transform': `translateY(${loadConfig().tagIconYOffset}px)`,
  1759. });
  1760. if (!labelString) {
  1761. iconEl.css({
  1762. marginLeft: '0',
  1763. marginRight: '0',
  1764. });
  1765. }
  1766. textEl.prepend(iconEl);
  1767. }
  1768.  
  1769. tagEl.append(textEl);
  1770.  
  1771. if (tag.link) {
  1772. const linkEl = jq('<a/>')
  1773. .attr('href', tag.link)
  1774. .attr('target', tag.linkTarget)
  1775. .css({
  1776. textDecoration: 'none',
  1777. color: 'inherit'
  1778. });
  1779. textEl.wrap(linkEl);
  1780. }
  1781.  
  1782. if (!isPreview && !tag.link && !tag.dir) {
  1783. tagEl.click(clickHandler);
  1784. }
  1785. containerEl.append(tagEl);
  1786.  
  1787. return tagEl;
  1788. };
  1789.  
  1790. const genDebugFakeTags = () =>
  1791. _.times(TAGS_PALETTE_COLORS_NUM, x => `Fake ${x} ${_.times(x / 3).map(() => 'x').join('')}<color:%${x % TAGS_PALETTE_COLORS_NUM}>`)
  1792. .join('\n');
  1793.  
  1794. const getTagContainerType = containerEl => {
  1795. if (containerEl.hasClass(threadTagContainerCls) || containerEl.hasClass(tagsPreviewThreadCls)) return TAG_CONTAINER_TYPE.THREAD;
  1796. if (containerEl.hasClass(newTagContainerCls) || containerEl.hasClass(tagsPreviewNewCls)) return TAG_CONTAINER_TYPE.NEW;
  1797. if (containerEl.hasClass(newTagContainerInCollectionCls) || containerEl.hasClass(tagsPreviewNewInCollectionCls)) return TAG_CONTAINER_TYPE.NEW_IN_COLLECTION;
  1798. return null;
  1799. };
  1800.  
  1801. const getPromptWrapperTagContainerType = promptWrapper => {
  1802. if (PP.getPromptAreaOfNewThread(promptWrapper).length) return TAG_CONTAINER_TYPE.NEW;
  1803. if (PP.getPromptAreaOnThread(promptWrapper).length) return TAG_CONTAINER_TYPE.THREAD;
  1804. if (PP.getPromptAreaOnCollection(promptWrapper).length) return TAG_CONTAINER_TYPE.NEW_IN_COLLECTION;
  1805. return null;
  1806. };
  1807.  
  1808. const isTagRelevantForContainer = containerType => tag =>
  1809. containerType === tag.target
  1810. || (containerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION && tag.target === TAG_CONTAINER_TYPE.NEW)
  1811. || tag.target === TAG_CONTAINER_TYPE.ALL;
  1812.  
  1813. const tagContainerTypeToTagContainerClass = {
  1814. [TAG_CONTAINER_TYPE.THREAD]: threadTagContainerCls,
  1815. [TAG_CONTAINER_TYPE.NEW]: newTagContainerCls,
  1816. [TAG_CONTAINER_TYPE.NEW_IN_COLLECTION]: newTagContainerInCollectionCls,
  1817. };
  1818.  
  1819. const currentUrlIsSettingsPage = () => window.location.pathname.includes('/settings/');
  1820.  
  1821. const refreshTags = ({ force = false } = {}) => {
  1822. if (!loadConfigOrDefault()?.tagsEnabled) return;
  1823. const promptWrapper = PP.getPromptAreaWrapperOfNewThread()
  1824. .add(PP.getPromptAreaWrapperOnThread())
  1825. .add(PP.getPromptAreaWrapperOnCollection())
  1826. .filter((_, rEl) => {
  1827. const isPreview = Boolean(jq(rEl).attr('data-preview'));
  1828. return isPreview || !currentUrlIsSettingsPage();
  1829. });
  1830. if (!promptWrapper.length) {
  1831. debugLogTags('no prompt area found');
  1832. }
  1833. // debugLogTags('promptWrappers', promptWrapper);
  1834. const allTags = _.flow(
  1835. x => x + (unsafeWindow.phFakeTags ? `${nl}${genDebugFakeTags()}${nl}` : ''),
  1836. parseTagsText,
  1837. )(loadConfig()?.tagsText ?? defaultConfig.tagsText);
  1838. debugLogTags('refreshing allTags', allTags);
  1839.  
  1840. const createContainer = (promptWrapper) => {
  1841. const el = jq(`<div/>`).addClass(tagsContainerCls);
  1842. const tagContainerType = getPromptWrapperTagContainerType(promptWrapper);
  1843. if (tagContainerType) {
  1844. const clsToAdd = tagContainerTypeToTagContainerClass[tagContainerType];
  1845. if (!clsToAdd) {
  1846. console.error('Unexpected tagContainerType:', tagContainerType, { promptWrapper });
  1847. }
  1848. el.addClass(clsToAdd);
  1849. }
  1850. return el;
  1851. };
  1852. promptWrapper.each((_, rEl) => {
  1853. const el = jq(rEl);
  1854. if (el.parent().find(`.${tagsContainerCls}`).length) {
  1855. el.parent().addClass(queryBoxCls);
  1856. return;
  1857. }
  1858. el.before(createContainer(el));
  1859. });
  1860.  
  1861. const currentPalette = getPalette(loadConfig().tagPalette);
  1862.  
  1863. const createFence = (fence) => {
  1864. const fenceEl = jq('<div/>')
  1865. .addClass(tagFenceCls)
  1866. .css({
  1867. 'border-style': fence.fenceBorderStyle ?? 'solid',
  1868. 'border-color': fence.fenceBorderColor?.startsWith('%')
  1869. ? convertColorInPaletteFormat(currentPalette)(fence.fenceBorderColor)
  1870. : fence.fenceBorderColor ?? defaultTagColor,
  1871. 'border-width': fence.fenceBorderWidth ?? '1px',
  1872. })
  1873. .attr('data-tag', JSON.stringify(fence))
  1874. ;
  1875. const fenceContentEl = jq('<div/>')
  1876. .addClass(tagFenceContentCls)
  1877. .css({
  1878. 'width': fence.fenceWidth ?? '',
  1879. })
  1880. ;
  1881. fenceEl.append(fenceContentEl);
  1882. return { fenceEl, fenceContentEl };
  1883. };
  1884.  
  1885. const createDirectory = () => {
  1886. const directoryEl = jq('<div/>').addClass(tagDirectoryCls);
  1887. const directoryContentEl = jq('<div/>').addClass(tagDirectoryContentCls);
  1888. directoryEl.append(directoryContentEl);
  1889. return { directoryEl, directoryContentEl };
  1890. };
  1891.  
  1892. const containerEls = getTagsContainer();
  1893. containerEls.each((_i, rEl) => {
  1894. const containerEl = jq(rEl);
  1895. const isPreview = Boolean(containerEl.attr('data-preview'));
  1896.  
  1897. const tagContainerTypeFromPromptWrapper = getPromptWrapperTagContainerType(containerEl.nthParent(2));
  1898. const prelimTagContainerType = getTagContainerType(containerEl);
  1899. if (tagContainerTypeFromPromptWrapper !== prelimTagContainerType && !isPreview) {
  1900. debugLog('tagContainerTypeFromPromptWrapper !== prelimTagContainerType', { tagContainerTypeFromPromptWrapper, prelimTagContainerType, containerEl, isPreview });
  1901. containerEl
  1902. .empty()
  1903. .removeClass(threadTagContainerCls, newTagContainerCls, newTagContainerInCollectionCls)
  1904. .addClass(tagContainerTypeToTagContainerClass[tagContainerTypeFromPromptWrapper])
  1905. ;
  1906. } else {
  1907. if (!isPreview) {
  1908. debugLogTags('tagContainerTypeFromPromptWrapper === prelimTagContainerType', { tagContainerTypeFromPromptWrapper, prelimTagContainerType, containerEl, isPreview });
  1909. }
  1910. }
  1911.  
  1912. // TODO: use something else than lodash/fp. in following functions it behaved randomly very weirdly
  1913. // e.g. partial application of map resulting in an empty array or sortBy sorting field name instead
  1914. // of input array. possibly inconsistent normal FP order of arguments
  1915. const mapParseAttrTag = xs => xs.map(el => JSON.parse(el.dataset.tag));
  1916. const sortByOriginalIndex = xs => [...xs].sort((a, b) => a.originalIndex - b.originalIndex);
  1917. const tagElsInCurrentContainer = containerEl.find(`.${tagCls}, .${tagFenceCls}`).toArray();
  1918. const filterOutHidden = filter(x => !x.hide);
  1919. const currentTags = _.flow(
  1920. mapParseAttrTag,
  1921. sortByOriginalIndex,
  1922. filterOutHidden,
  1923. _.uniq,
  1924. )(tagElsInCurrentContainer);
  1925. const tagContainerType = getTagContainerType(containerEl);
  1926. const tagsForThisContainer = _.flow(
  1927. filter(isTagRelevantForContainer(tagContainerType)),
  1928. filterOutHidden,
  1929. sortByOriginalIndex,
  1930. )(allTags);
  1931. debugLogTags('tagContainerType =', tagContainerType, ', current tags =', currentTags, ', tagsForThisContainer =', tagsForThisContainer, ', tagElsInCurrentContainer =', tagElsInCurrentContainer);
  1932. if (_.isEqual(currentTags, tagsForThisContainer) && !force) {
  1933. debugLogTags('no tags changed');
  1934. return;
  1935. }
  1936. const diff = jsondiffpatch.diff(currentTags, tagsForThisContainer);
  1937. const changedTags = jsondiffpatch.formatters.console.format(diff);
  1938. debugLogTags('changedTags', changedTags);
  1939. containerEl.empty();
  1940. const tagHomePageLayout = loadConfig()?.tagHomePageLayout;
  1941. if (!isPreview) {
  1942. if ((tagContainerType === TAG_CONTAINER_TYPE.NEW || tagContainerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION)) {
  1943. if (tagContainerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION) {
  1944. // only compact layout is supported for new in collection
  1945. if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) {
  1946. containerEl.addClass(tagContainerCompactCls);
  1947. }
  1948. } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) {
  1949. containerEl.addClass(tagContainerCompactCls);
  1950. } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDER) {
  1951. containerEl.addClass(tagContainerWiderCls);
  1952. } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDE) {
  1953. containerEl.addClass(tagContainerWideCls);
  1954. } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.EXTRA_WIDE) {
  1955. containerEl.addClass(tagContainerExtraWideCls);
  1956. } else {
  1957. containerEl.removeClass(`${tagContainerCompactCls} ${tagContainerWiderCls} ${tagContainerWideCls} ${tagContainerExtraWideCls}`);
  1958. }
  1959. const extraMargin = loadConfig()?.tagContainerExtraBottomMargin || 0;
  1960. containerEl.css('margin-bottom', `${extraMargin}em`);
  1961. }
  1962. }
  1963.  
  1964. const fences = {};
  1965. const directories = {};
  1966.  
  1967. const fencesWrapperEl = jq('<div/>').addClass(tagAllFencesWrapperCls);
  1968. const restWrapperEl = jq('<div/>').addClass(tagRestOfTagsWrapperCls);
  1969.  
  1970. tagsForThisContainer.forEach(tag => {
  1971. const { fence, dir, inFence, inDir } = tag;
  1972.  
  1973. const getOrCreateDirectory = dirName => {
  1974. if (!directories[dirName]) directories[dirName] = createDirectory();
  1975. return directories[dirName];
  1976. };
  1977.  
  1978. const getTagContainer = () => {
  1979. if (fence) {
  1980. if (!fences[fence]) fences[fence] = createFence(tag);
  1981. return fences[fence].fenceContentEl;
  1982. } else if (dir && inFence) {
  1983. if (!fences[inFence]) {
  1984. console.error(`fence ${inFence} for tag not found`, tag);
  1985. return null;
  1986. }
  1987. const { directoryEl } = getOrCreateDirectory(dir);
  1988. fences[inFence].fenceContentEl.append(directoryEl);
  1989. return directoryEl;
  1990. } else if (dir) {
  1991. const { directoryEl } = getOrCreateDirectory(dir);
  1992. restWrapperEl.append(directoryEl);
  1993. return directoryEl;
  1994. } else if (inFence) {
  1995. if (!fences[inFence]) {
  1996. console.error(`fence ${inFence} for tag not found`, tag);
  1997. return null;
  1998. }
  1999. return fences[inFence].fenceContentEl;
  2000. } else if (inDir) {
  2001. if (!directories[inDir]) {
  2002. console.error(`directory ${inDir} for tag not found`, tag);
  2003. return null;
  2004. }
  2005. return directories[inDir].directoryContentEl;
  2006. } else {
  2007. return restWrapperEl;
  2008. }
  2009. };
  2010.  
  2011. const tagContainer = getTagContainer();
  2012. if (tagContainer && !fence) {
  2013. createTag(tagContainer)(isPreview)(tag);
  2014. }
  2015. });
  2016.  
  2017. Object.values(fences).forEach(({ fenceEl }) => fencesWrapperEl.append(fenceEl));
  2018. containerEl.append(fencesWrapperEl).append(restWrapperEl);
  2019. });
  2020. };
  2021.  
  2022. const setupTags = () => {
  2023. debugLog('setting up tags');
  2024. setInterval(refreshTags, 500);
  2025. };
  2026.  
  2027. const ICON_REPLACEMENT_MODE = Object.freeze({
  2028. OFF: 'Off',
  2029. LUCIDE1: 'Lucide 1',
  2030. LUCIDE2: 'Lucide 2',
  2031. LUCIDE3: 'Lucide 3',
  2032. TDESIGN1: 'TDesign 1',
  2033. TDESIGN2: 'TDesign 2',
  2034. TDESIGN3: 'TDesign 3',
  2035. });
  2036.  
  2037. const leftPanelIconMappingsToLucide1 = Object.freeze({
  2038. 'search': 'search',
  2039. 'discover': 'telescope',
  2040. 'spaces': 'shapes',
  2041. });
  2042.  
  2043. const leftPanelIconMappingsToLucide2 = Object.freeze({
  2044. 'search': 'house',
  2045. 'discover': 'compass',
  2046. 'spaces': 'square-stack',
  2047. 'library': 'archive',
  2048. });
  2049.  
  2050. const leftPanelIconMappingsToLucide3 = Object.freeze({
  2051. 'search': 'search',
  2052. 'discover': 'telescope',
  2053. 'spaces': 'bot',
  2054. 'library': 'folder-open',
  2055. });
  2056.  
  2057. const leftPanelIconMappingsToTDesign1 = Object.freeze({
  2058. 'search': 'search',
  2059. 'discover': 'compass-filled',
  2060. 'spaces': 'grid-view',
  2061. 'library': 'book',
  2062. });
  2063.  
  2064. const leftPanelIconMappingsToTDesign2 = Object.freeze({
  2065. 'search': 'search',
  2066. 'discover': 'shutter-filled',
  2067. 'spaces': 'palette-1',
  2068. 'library': 'folder-open-1-filled',
  2069. });
  2070.  
  2071. const leftPanelIconMappingsToTDesign3 = Object.freeze({
  2072. 'search': 'search',
  2073. 'discover': 'banana-filled',
  2074. 'spaces': 'chili-filled',
  2075. 'library': 'barbecue-filled',
  2076. });
  2077.  
  2078. const iconMappings = {
  2079. LUCIDE1: leftPanelIconMappingsToLucide1,
  2080. LUCIDE2: leftPanelIconMappingsToLucide2,
  2081. LUCIDE3: leftPanelIconMappingsToLucide3,
  2082. TDESIGN1: leftPanelIconMappingsToTDesign1,
  2083. TDESIGN2: leftPanelIconMappingsToTDesign2,
  2084. TDESIGN3: leftPanelIconMappingsToTDesign3,
  2085. };
  2086.  
  2087. const MODEL_LABEL_TEXT_MODE = Object.freeze({
  2088. OFF: 'Off',
  2089. FULL_NAME: 'Full Name',
  2090. SHORT_NAME: 'Short Name',
  2091. PP_MODEL_ID: 'PP Model ID',
  2092. OWN_NAME_VERSION_SHORT: 'Own Name + Version Short',
  2093. VERY_SHORT: 'Very Short',
  2094. FAMILIAR_NAME: 'Familiar Name',
  2095. });
  2096.  
  2097. const MODEL_LABEL_STYLE = Object.freeze({
  2098. OFF: 'Off',
  2099. NO_TEXT: 'No text',
  2100. JUST_TEXT: 'Just Text',
  2101. BUTTON_SUBTLE: 'Button Subtle',
  2102. BUTTON_WHITE: 'Button White',
  2103. BUTTON_CYAN: 'Button Cyan',
  2104. });
  2105.  
  2106. const CUSTOM_MODEL_POPOVER_MODE = Object.freeze({
  2107. OFF: 'Off',
  2108. SIMPLE_LIST: 'Simple List',
  2109. COMPACT_LIST: 'Compact List',
  2110. SIMPLE_GRID: 'Simple 2x Grid',
  2111. COMPACT_GRID: 'Compact 2x Grid',
  2112. });
  2113.  
  2114. const MODEL_LABEL_ICON_REASONING_MODEL = Object.freeze({
  2115. OFF: 'Off',
  2116. LIGHTBULB: 'Lightbulb',
  2117. BRAIN: 'Brain',
  2118. MICROCHIP: 'Microchip',
  2119. COG: 'Cog',
  2120. BRAIN_COG: 'Brain Cog',
  2121. CALCULATOR: 'Calculator',
  2122. BOT: 'Bot',
  2123. });
  2124.  
  2125. const MODEL_LABEL_ICONS = Object.freeze({
  2126. OFF: 'Off',
  2127. MONOCHROME: 'Monochrome',
  2128. COLOR: 'Color',
  2129. });
  2130.  
  2131. const defaultConfig = Object.freeze({
  2132. // General
  2133. hideSideMenu: false,
  2134. slimLeftMenu: false,
  2135. hideSideMenuLabels: false,
  2136. hideHomeWidgets: false,
  2137. hideDiscoverButton: false,
  2138. fixImageGenerationOverlay: false,
  2139. extraSpaceBellowLastAnswer: false,
  2140. replaceIconsInMenu: ICON_REPLACEMENT_MODE.OFF,
  2141. leftMarginOfThreadContent: null,
  2142.  
  2143. // Model
  2144. modelLabelTextMode: MODEL_LABEL_TEXT_MODE.OFF,
  2145. modelLabelStyle: MODEL_LABEL_STYLE.OFF,
  2146. modelLabelOverwriteCyanIconToGray: false,
  2147. modelLabelUseIconForReasoningModels: MODEL_LABEL_ICON_REASONING_MODEL.OFF,
  2148. modelLabelReasoningModelIconColor: '#ffffff',
  2149. modelLabelRemoveCpuIcon: false,
  2150. modelLabelLargerIcons: false,
  2151. modelLabelIcons: MODEL_LABEL_ICONS.OFF,
  2152. customModelPopover: CUSTOM_MODEL_POPOVER_MODE.SIMPLE_GRID,
  2153.  
  2154. // Legacy
  2155. showCopilot: true,
  2156. showCopilotNewThread: true,
  2157. showCopilotRepeatLast: true,
  2158. showCopilotCopyPlaceholder: true,
  2159.  
  2160. // Tags
  2161. tagsEnabled: true,
  2162. tagsText: '',
  2163. tagPalette: 'CLASSIC',
  2164. tagPaletteCustom: ['#000', '#fff', '#ff0', '#f00', '#0f0', '#00f', '#0ff', '#f0f'],
  2165. tagFont: 'Roboto',
  2166. tagHomePageLayout: TAG_HOME_PAGE_LAYOUT.DEFAULT,
  2167. tagContainerExtraBottomMargin: 0,
  2168. tagLuminanceThreshold: 0.35,
  2169. tagBold: false,
  2170. tagItalic: false,
  2171. tagFontSize: 16,
  2172. tagIconSize: 16,
  2173. tagRoundness: 4,
  2174. tagTextYOffset: 0,
  2175. tagIconYOffset: 0,
  2176. tagToggleSave: false,
  2177. toggleModeHooks: true,
  2178. tagToggleModeIndicators: true,
  2179. tagToggledStates: {}, // Store toggle states by tag identifier
  2180.  
  2181. // Raw
  2182. mainCaptionHtml: '',
  2183. mainCaptionHtmlEnabled: false,
  2184. customJs: '',
  2185. customJsEnabled: false,
  2186. customCss: '',
  2187. customCssEnabled: false,
  2188. customWidgetsHtml: '',
  2189. customWidgetsHtmlEnabled: false,
  2190.  
  2191. // Settings
  2192. activeSettingsTab: 'general',
  2193.  
  2194. // Debug
  2195. debugMode: false,
  2196. debugTagsMode: false,
  2197. debugTagsSuppressSubmit: false,
  2198. autoOpenSettings: false,
  2199. });
  2200.  
  2201. // TODO: if still using local storage, at least it should be prefixed with user script name
  2202. const storageKey = 'checkBoxStates';
  2203.  
  2204. const loadConfig = () => {
  2205. try {
  2206. // TODO: use storage from GM API
  2207. const val = JSON.parse(localStorage.getItem(storageKey));
  2208. // debugLog('loaded config', val);
  2209. return val;
  2210. } catch (e) {
  2211. console.error('Failed to load config, using default', e);
  2212. return defaultConfig;
  2213. }
  2214. };
  2215.  
  2216. const loadConfigOrDefault = () => loadConfig() ?? defaultConfig;
  2217.  
  2218. const saveConfig = cfg => {
  2219. debugLog('saving config', cfg);
  2220. localStorage.setItem(storageKey, JSON.stringify(cfg));
  2221. };
  2222.  
  2223. const createCheckbox = (id, labelText, onChange) => {
  2224. debugLog("createCheckbox", id);
  2225. const checkbox = jq(`<input type="checkbox" id=${id}>`);
  2226. const label = jq(`<label class="checkbox_label" for="${id}">${labelText}</label>`);
  2227. const checkboxWithLabel = jq('<div class="checkbox_wrapper"></div>').append(checkbox).append(' ').append(label);
  2228. debugLog('checkboxwithlabel', checkboxWithLabel);
  2229.  
  2230. getSettingsLastTabGroupContent().append(checkboxWithLabel);
  2231. checkbox.on('change', onChange);
  2232. return checkbox;
  2233. };
  2234.  
  2235. const createTextArea = (id, labelText, onChange, helpText, links) => {
  2236. debugLog("createTextArea", id);
  2237. const textarea = jq(`<textarea id=${id}></textarea>`);
  2238. const bookIconHtml = `<img src="${getLucideIconUrl('book-text')}" class="w-4 h-4 invert inline-block"/>`;
  2239. const labelTextHtml = `<span class="opacity-100">${labelText}</span>`;
  2240. const label = jq(`<label class="textarea_label">${labelTextHtml}${helpText ? ' ' + bookIconHtml : ''}</label>`);
  2241. const labelWithLinks = jq('<div/>').addClass('flex flex-row gap-2 mb-2').append(label);
  2242. const textareaWrapper = jq('<div class="textarea_wrapper"></div>').append(labelWithLinks);
  2243. if (links) {
  2244. links.forEach(({ icon, label, url, tooltip }) => {
  2245. const iconHtml = `<img src="${getIconUrl(icon)}" class="w-4 h-4 invert opacity-50 hover:opacity-100 transition-opacity duration-300 ease-in-out"/>`;
  2246. const link = jq(`<a href="${url}" target="_blank" class="flex flex-row gap-2 items-center">${icon ? iconHtml : ''}${label ? ' ' + label : ''}</a>`);
  2247. link.attr('title', tooltip);
  2248. labelWithLinks.append(link);
  2249. });
  2250. }
  2251. if (helpText) {
  2252. const help = jq(`<div/>`).addClass(helpTextCls).html(markdownConverter.makeHtml(helpText)).append(jq('<br/>'));
  2253. help.find('a').each((_, a) => jq(a).attr('target', '_blank'));
  2254. help.append(jq('<button/>').text('[Close help]').on('click', () => help.hide()));
  2255. textareaWrapper.append(help);
  2256. label
  2257. .css({ cursor: 'pointer' })
  2258. .on('click', () => help.toggle())
  2259. .prop('title', 'Click to toggle help')
  2260. ;
  2261. help.hide();
  2262. }
  2263. textareaWrapper.append(textarea);
  2264. debugLog('textareaWithLabel', textareaWrapper);
  2265.  
  2266. getSettingsLastTabGroupContent().append(textareaWrapper);
  2267. textarea.on('change', onChange);
  2268. return textarea;
  2269. };
  2270.  
  2271. const createSelect = (id, labelText, options, onChange) => {
  2272. const select = jq(`<select id=${id}>`);
  2273. options.forEach(({ value, label }) => {
  2274. jq('<option>').val(value).text(label).appendTo(select);
  2275. });
  2276. const label = jq(`<label class="select_label">${labelText}</label>`);
  2277. const selectWithLabel = jq('<div class="select_wrapper"></div>').append(select).append(label);
  2278. debugLog('selectWithLabel', selectWithLabel);
  2279.  
  2280. getSettingsLastTabGroupContent().append(selectWithLabel);
  2281. select.on('change', onChange);
  2282. return select;
  2283. };
  2284.  
  2285. const createPaletteLegend = paletteName => {
  2286. const wrapper = jq('<div/>')
  2287. .addClass(tagPaletteCls)
  2288. .append(jq('<span>').html('Palette of color codes:&nbsp;'))
  2289. ;
  2290. const palette = getPalette(paletteName);
  2291. palette.forEach((color, i) => {
  2292. const colorCode = `%${i}`;
  2293. const colorPart = genColorPart(colorCode);
  2294. // console.log('createPaletteLegend', {i, colorCode, colorPart, color});
  2295. jq('<span/>')
  2296. .text(colorCode)
  2297. .addClass(tagPaletteItemCls)
  2298. .css({
  2299. 'background-color': color,
  2300. })
  2301. .prop('title', `Copy ${colorPart} to clipboard`)
  2302. .click(() => {
  2303. copyTextToClipboard(colorPart);
  2304. })
  2305. .appendTo(wrapper);
  2306. });
  2307. return wrapper;
  2308. };
  2309.  
  2310. const createColorInput = (id, labelText, onChange) => {
  2311. debugLog("createColorInput", id);
  2312. const input = jq(`<input type="color" id=${id}>`);
  2313. const label = jq(`<label class="color_label">${labelText}</label>`);
  2314. const inputWithLabel = jq('<div class="color_wrapper"></div>').append(input).append(label);
  2315. debugLog('inputWithLabel', inputWithLabel);
  2316.  
  2317. getSettingsLastTabGroupContent().append(inputWithLabel);
  2318. input.on('change', onChange);
  2319. return input;
  2320. };
  2321.  
  2322. const createNumberInput = (id, labelText, onChange, { step = 1, min = 0, max = 100 } = {}) => {
  2323. debugLog("createNumberInput", id);
  2324. const input = jq(`<input type="number" id=${id}>`)
  2325. .prop('step', step)
  2326. .prop('min', min)
  2327. .prop('max', max)
  2328. ;
  2329. const label = jq(`<label class="number_label">${labelText}</label>`);
  2330. const inputWithLabel = jq('<div class="number_wrapper"></div>').append(input).append(label);
  2331. debugLog('inputWithLabel', inputWithLabel);
  2332.  
  2333. getSettingsLastTabGroupContent().append(inputWithLabel);
  2334. input.on('change', onChange);
  2335. return input;
  2336. };
  2337.  
  2338. const createTagsPreview = () => {
  2339. const wrapper = jq('<div/>')
  2340. .addClass(tagsPreviewCls)
  2341. .append(jq('<div>').text('Preview').addClass('text-lg font-bold'))
  2342. .append(jq('<div>').text('Target New:'))
  2343. .append(jq('<div>').addClass(tagsPreviewNewCls).addClass(tagsContainerCls).attr('data-preview', 'true'))
  2344. .append(jq('<div>').text('Target Thread:'))
  2345. .append(jq('<div>').addClass(tagsPreviewThreadCls).addClass(tagsContainerCls).attr('data-preview', 'true'))
  2346. ;
  2347. getSettingsLastTabGroupContent().append(wrapper);
  2348. };
  2349.  
  2350. const coPilotNewThreadAutoSubmitCheckboxId = 'coPilotNewThreadAutoSubmit';
  2351. const getCoPilotNewThreadAutoSubmitCheckbox = () => $i(coPilotNewThreadAutoSubmitCheckboxId);
  2352.  
  2353. const coPilotRepeatLastAutoSubmitCheckboxId = 'coPilotRepeatLastAutoSubmit';
  2354. const getCoPilotRepeatLastAutoSubmitCheckbox = () => $i(coPilotRepeatLastAutoSubmitCheckboxId);
  2355.  
  2356. const hideSideMenuCheckboxId = 'hideSideMenu';
  2357. const getHideSideMenuCheckbox = () => $i(hideSideMenuCheckboxId);
  2358.  
  2359. const tagsEnabledId = genCssName('tagsEnabled');
  2360. const getTagsEnabledCheckbox = () => $i(tagsEnabledId);
  2361.  
  2362. const tagsTextAreaId = 'tagsText';
  2363. const getTagsTextArea = () => $i(tagsTextAreaId);
  2364.  
  2365. const tagColorPickerId = genCssName('tagColorPicker');
  2366. const getTagColorPicker = () => $i(tagColorPickerId);
  2367.  
  2368. const enableDebugCheckboxId = genCssName('enableDebug');
  2369. const getEnableDebugCheckbox = () => $i(enableDebugCheckboxId);
  2370.  
  2371. const enableTagsDebugCheckboxId = genCssName('enableTagsDebug');
  2372. const getEnableTagsDebugCheckbox = () => $i(enableTagsDebugCheckboxId);
  2373.  
  2374. const debugTagsSuppressSubmitCheckboxId = genCssName('debugTagsSuppressSubmit');
  2375. const getDebugTagsSuppressSubmitCheckbox = () => $i(debugTagsSuppressSubmitCheckboxId);
  2376.  
  2377. const tagPaletteSelectId = genCssName('tagPaletteSelect');
  2378. const getTagPaletteSelect = () => $i(tagPaletteSelectId);
  2379.  
  2380. const tagFontSelectId = genCssName('tagFontSelect');
  2381. const getTagFontSelect = () => $i(tagFontSelectId);
  2382.  
  2383. const tagTweakNoBorderCheckboxId = genCssName('tagTweakNoBorder');
  2384. const getTagTweakNoBorderCheckbox = () => $i(tagTweakNoBorderCheckboxId);
  2385.  
  2386. const tagTweakSlimPaddingCheckboxId = genCssName('tagTweakSlimPadding');
  2387. const getTagTweakSlimPaddingCheckbox = () => $i(tagTweakSlimPaddingCheckboxId);
  2388.  
  2389. const tagTweakRichBorderColorCheckboxId = genCssName('tagTweakRichBorderColor');
  2390. const getTagTweakRichBorderColorCheckbox = () => $i(tagTweakRichBorderColorCheckboxId);
  2391.  
  2392. const tagTweakTextShadowCheckboxId = genCssName('tagTweakTextShadow');
  2393. const getTagTweakTextShadowCheckbox = () => $i(tagTweakTextShadowCheckboxId);
  2394.  
  2395. const tagHomePageLayoutSelectId = genCssName('tagHomePageLayout');
  2396. const getTagHomePageLayoutSelect = () => $i(tagHomePageLayoutSelectId);
  2397.  
  2398. const tagContainerExtraBottomMarginInputId = genCssName('tagContainerExtraBottomMargin');
  2399. const getTagContainerExtraBottomMarginInput = () => $i(tagContainerExtraBottomMarginInputId);
  2400.  
  2401. const tagLuminanceThresholdInputId = genCssName('tagLuminanceThreshold');
  2402. const getTagLuminanceThresholdInput = () => $i(tagLuminanceThresholdInputId);
  2403.  
  2404. const tagBoldCheckboxId = genCssName('tagBold');
  2405. const getTagBoldCheckbox = () => $i(tagBoldCheckboxId);
  2406.  
  2407. const tagItalicCheckboxId = genCssName('tagItalic');
  2408. const getTagItalicCheckbox = () => $i(tagItalicCheckboxId);
  2409.  
  2410. const tagFontSizeInputId = genCssName('tagFontSize');
  2411. const getTagFontSizeInput = () => $i(tagFontSizeInputId);
  2412.  
  2413. const tagIconSizeInputId = genCssName('tagIconSize');
  2414. const getTagIconSizeInput = () => $i(tagIconSizeInputId);
  2415.  
  2416. const tagRoundnessInputId = genCssName('tagRoundness');
  2417. const getTagRoundnessInput = () => $i(tagRoundnessInputId);
  2418.  
  2419. const tagTextYOffsetInputId = genCssName('tagTextYOffset');
  2420. const getTagTextYOffsetInput = () => $i(tagTextYOffsetInputId);
  2421.  
  2422. const tagIconYOffsetInputId = genCssName('tagIconYOffset');
  2423. const getTagIconYOffsetInput = () => $i(tagIconYOffsetInputId);
  2424.  
  2425. const tagToggleSaveCheckboxId = genCssName('tagToggleSave');
  2426. const getTagToggleSaveCheckbox = () => $i(tagToggleSaveCheckboxId);
  2427.  
  2428. const toggleModeHooksCheckboxId = genCssName('toggleModeHooks');
  2429. const getToggleModeHooksCheckbox = () => $i(toggleModeHooksCheckboxId);
  2430.  
  2431. const tagToggleModeIndicatorsCheckboxId = genCssName('tagToggleModeIndicators');
  2432. const getTagToggleModeIndicatorsCheckbox = () => $i(tagToggleModeIndicatorsCheckboxId);
  2433.  
  2434. const tagPaletteCustomTextAreaId = genCssName('tagPaletteCustomTextArea');
  2435. const getTagPaletteCustomTextArea = () => $i(tagPaletteCustomTextAreaId);
  2436.  
  2437. const replaceIconsInMenuId = genCssName('replaceIconsInMenu');
  2438. const getReplaceIconsInMenu = () => $i(replaceIconsInMenuId);
  2439.  
  2440. const slimLeftMenuCheckboxId = genCssName('slimLeftMenu');
  2441. const getSlimLeftMenuCheckbox = () => $i(slimLeftMenuCheckboxId);
  2442.  
  2443. const leftMarginOfThreadContentInputId = genCssName('leftMarginOfThreadContent');
  2444. const getLeftMarginOfThreadContentInput = () => $i(leftMarginOfThreadContentInputId);
  2445.  
  2446. const hideHomeWidgetsCheckboxId = genCssName('hideHomeWidgets');
  2447. const getHideHomeWidgetsCheckbox = () => $i(hideHomeWidgetsCheckboxId);
  2448.  
  2449. const hideDiscoverButtonCheckboxId = genCssName('hideDiscoverButton');
  2450. const getHideDiscoverButtonCheckbox = () => $i(hideDiscoverButtonCheckboxId);
  2451.  
  2452. const fixImageGenerationOverlayCheckboxId = genCssName('fixImageGenerationOverlay');
  2453. const getFixImageGenerationOverlayCheckbox = () => $i(fixImageGenerationOverlayCheckboxId);
  2454.  
  2455. const extraSpaceBellowLastAnswerCheckboxId = genCssName('extraSpaceBellowLastAnswer');
  2456. const getExtraSpaceBellowLastAnswerCheckbox = () => $i(extraSpaceBellowLastAnswerCheckboxId);
  2457.  
  2458. const modelLabelTextModeSelectId = genCssName('modelLabelTextModeSelect');
  2459. const getModelLabelTextModeSelect = () => $i(modelLabelTextModeSelectId);
  2460.  
  2461. const modelLabelStyleSelectId = genCssName('modelLabelStyleSelect');
  2462. const getModelLabelStyleSelect = () => $i(modelLabelStyleSelectId);
  2463.  
  2464. const modelLabelOverwriteCyanIconToGrayCheckboxId = genCssName('modelLabelOverwriteCyanIconToGray');
  2465. const getModelLabelOverwriteCyanIconToGrayCheckbox = () => $i(modelLabelOverwriteCyanIconToGrayCheckboxId);
  2466.  
  2467. const modelLabelUseIconForReasoningModelsSelectId = genCssName('modelLabelUseIconForReasoningModelsSelect');
  2468. const getModelLabelUseIconForReasoningModelsSelect = () => $i(modelLabelUseIconForReasoningModelsSelectId);
  2469.  
  2470. const modelLabelReasoningModelIconColorId = genCssName('modelLabelReasoningModelIconColor');
  2471. const getModelLabelReasoningModelIconColor = () => $i(modelLabelReasoningModelIconColorId);
  2472.  
  2473. const modelLabelRemoveCpuIconCheckboxId = genCssName('modelLabelRemoveCpuIconCheckbox');
  2474. const getModelLabelRemoveCpuIconCheckbox = () => $i(modelLabelRemoveCpuIconCheckboxId);
  2475.  
  2476. const modelLabelLargerIconsCheckboxId = genCssName('modelLabelLargerIconsCheckbox');
  2477. const getModelLabelLargerIconsCheckbox = () => $i(modelLabelLargerIconsCheckboxId);
  2478.  
  2479. const modelLabelIconsSelectId = genCssName('modelLabelIconsSelect');
  2480. const getModelLabelIconsSelect = () => $i(modelLabelIconsSelectId);
  2481.  
  2482. const customModelPopoverSelectId = genCssName('customModelPopoverSelect');
  2483. const getCustomModelPopoverSelect = () => $i(customModelPopoverSelectId);
  2484.  
  2485. const mainCaptionHtmlTextAreaId = genCssName('mainCaptionHtmlTextArea');
  2486. const getMainCaptionHtmlTextArea = () => $i(mainCaptionHtmlTextAreaId);
  2487.  
  2488. const customJsTextAreaId = genCssName('customJsTextArea');
  2489. const getCustomJsTextArea = () => $i(customJsTextAreaId);
  2490.  
  2491. const customCssTextAreaId = genCssName('customCssTextArea');
  2492. const getCustomCssTextArea = () => $i(customCssTextAreaId);
  2493.  
  2494. const customWidgetsHtmlTextAreaId = genCssName('customWidgetsHtmlTextArea');
  2495. const getCustomWidgetsHtmlTextArea = () => $i(customWidgetsHtmlTextAreaId);
  2496.  
  2497. const mainCaptionHtmlEnabledId = genCssName('mainCaptionHtmlEnabled');
  2498. const customJsEnabledId = genCssName('customJsEnabled');
  2499. const customCssEnabledId = genCssName('customCssEnabled');
  2500. const customWidgetsHtmlEnabledId = genCssName('customWidgetsHtmlEnabled');
  2501.  
  2502. const getMainCaptionHtmlEnabledCheckbox = () => $i(mainCaptionHtmlEnabledId);
  2503. const getCustomJsEnabledCheckbox = () => $i(customJsEnabledId);
  2504. const getCustomCssEnabledCheckbox = () => $i(customCssEnabledId);
  2505. const getCustomWidgetsHtmlEnabledCheckbox = () => $i(customWidgetsHtmlEnabledId);
  2506.  
  2507. const hideSideMenuLabelsId = genCssName('hideSideMenuLabels');
  2508. const getHideSideMenuLabels = () => $i(hideSideMenuLabelsId);
  2509.  
  2510. const autoOpenSettingsCheckboxId = genCssName('autoOpenSettings');
  2511. const getAutoOpenSettingsCheckbox = () => $i(autoOpenSettingsCheckboxId);
  2512.  
  2513. const copyTextToClipboard = async text => {
  2514. try {
  2515. await navigator.clipboard.writeText(text);
  2516. console.log('Text copied to clipboard', { text });
  2517. } catch (err) {
  2518. console.error('Failed to copy text: ', err);
  2519. }
  2520. };
  2521.  
  2522. const genColorPart = color => `<color:${color}>`;
  2523.  
  2524. const loadCurrentConfigToSettingsForm = () => {
  2525. const savedStatesRaw = JSON.parse(localStorage.getItem(storageKey));
  2526. if (savedStatesRaw === null) { return; }
  2527. const savedStates = { ...defaultConfig, ...savedStatesRaw };
  2528.  
  2529. getCoPilotNewThreadAutoSubmitCheckbox().prop('checked', savedStates.coPilotNewThreadAutoSubmit);
  2530. getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked', savedStates.coPilotRepeatLastAutoSubmit);
  2531. getHideSideMenuCheckbox().prop('checked', savedStates.hideSideMenu);
  2532. getTagsEnabledCheckbox().prop('checked', savedStates.tagsEnabled);
  2533. getTagsTextArea().val(savedStates.tagsText);
  2534. getEnableDebugCheckbox().prop('checked', savedStates.debugMode);
  2535. getEnableTagsDebugCheckbox().prop('checked', savedStates.debugTagsMode);
  2536. getDebugTagsSuppressSubmitCheckbox().prop('checked', savedStates.debugTagsSuppressSubmit);
  2537. getTagPaletteSelect().val(savedStates.tagPalette);
  2538. getTagFontSelect().val(savedStates.tagFont);
  2539. getTagTweakNoBorderCheckbox().prop('checked', savedStates.tagTweakNoBorder);
  2540. getTagTweakSlimPaddingCheckbox().prop('checked', savedStates.tagTweakSlimPadding);
  2541. getTagTweakRichBorderColorCheckbox().prop('checked', savedStates.tagTweakRichBorderColor);
  2542. getTagTweakTextShadowCheckbox().prop('checked', savedStates.tagTweakTextShadow);
  2543. getTagHomePageLayoutSelect().val(savedStates.tagHomePageLayout);
  2544. getTagContainerExtraBottomMarginInput().val(savedStates.tagContainerExtraBottomMargin);
  2545. getTagLuminanceThresholdInput().val(savedStates.tagLuminanceThreshold);
  2546. getTagBoldCheckbox().prop('checked', savedStates.tagBold);
  2547. getTagItalicCheckbox().prop('checked', savedStates.tagItalic);
  2548. getTagFontSizeInput().val(savedStates.tagFontSize);
  2549. getTagIconSizeInput().val(savedStates.tagIconSize);
  2550. getTagTextYOffsetInput().val(savedStates.tagTextYOffset);
  2551. getTagIconYOffsetInput().val(savedStates.tagIconYOffset);
  2552. getTagRoundnessInput().val(savedStates.tagRoundness);
  2553. getTagToggleSaveCheckbox().prop('checked', savedStates.tagToggleSave);
  2554. getToggleModeHooksCheckbox().prop('checked', savedStates.toggleModeHooks);
  2555. getTagToggleModeIndicatorsCheckbox().prop('checked', savedStates.tagToggleModeIndicators);
  2556. getReplaceIconsInMenu().val(savedStates.replaceIconsInMenu);
  2557. getSlimLeftMenuCheckbox().prop('checked', savedStates.slimLeftMenu);
  2558. getHideHomeWidgetsCheckbox().prop('checked', savedStates.hideHomeWidgets);
  2559. getHideDiscoverButtonCheckbox().prop('checked', savedStates.hideDiscoverButton);
  2560. getFixImageGenerationOverlayCheckbox().prop('checked', savedStates.fixImageGenerationOverlay);
  2561. getExtraSpaceBellowLastAnswerCheckbox().prop('checked', savedStates.extraSpaceBellowLastAnswer);
  2562. getModelLabelTextModeSelect().val(savedStates.modelLabelTextMode);
  2563. getModelLabelStyleSelect().val(savedStates.modelLabelStyle);
  2564. getModelLabelRemoveCpuIconCheckbox().prop('checked', savedStates.modelLabelRemoveCpuIcon);
  2565. getModelLabelLargerIconsCheckbox().prop('checked', savedStates.modelLabelLargerIcons);
  2566. getModelLabelOverwriteCyanIconToGrayCheckbox().prop('checked', savedStates.modelLabelOverwriteCyanIconToGray);
  2567. getModelLabelUseIconForReasoningModelsSelect().val(savedStates.modelLabelUseIconForReasoningModels ?? MODEL_LABEL_ICON_REASONING_MODEL.OFF);
  2568. getModelLabelReasoningModelIconColor().val(savedStates.modelLabelReasoningModelIconColor || '#ffffff');
  2569. getModelLabelIconsSelect().val(savedStates.modelLabelIcons ?? MODEL_LABEL_ICONS.OFF);
  2570. getCustomModelPopoverSelect().val(savedStates.customModelPopover ?? CUSTOM_MODEL_POPOVER_MODE.SIMPLE_GRID);
  2571. getTagPaletteCustomTextArea().val((savedStates.tagPaletteCustom || []).join(', '));
  2572. getMainCaptionHtmlTextArea().val(savedStates.mainCaptionHtml);
  2573. getCustomJsTextArea().val(savedStates.customJs);
  2574. getCustomCssTextArea().val(savedStates.customCss);
  2575. getCustomWidgetsHtmlTextArea().val(savedStates.customWidgetsHtml);
  2576. getMainCaptionHtmlEnabledCheckbox().prop('checked', savedStates.mainCaptionHtmlEnabled);
  2577. getCustomJsEnabledCheckbox().prop('checked', savedStates.customJsEnabled);
  2578. getCustomCssEnabledCheckbox().prop('checked', savedStates.customCssEnabled);
  2579. getCustomWidgetsHtmlEnabledCheckbox().prop('checked', savedStates.customWidgetsHtmlEnabled);
  2580. getHideSideMenuLabels().prop('checked', savedStates.hideSideMenuLabels);
  2581. getLeftMarginOfThreadContentInput().val(savedStates.leftMarginOfThreadContent);
  2582. getAutoOpenSettingsCheckbox().prop('checked', savedStates.autoOpenSettings);
  2583. };
  2584.  
  2585. function handleSettingsInit() {
  2586. const modalExists = getPerplexityHelperModal().length > 0;
  2587. const firstCheckboxExists = getCoPilotNewThreadAutoSubmitCheckbox().length > 0;
  2588.  
  2589. if (!modalExists || firstCheckboxExists) { return; }
  2590.  
  2591. const $tabButtons = $c(modalTabGroupTabsCls).addClass('flex gap-2 items-end');
  2592.  
  2593. const setActiveTab = (tabName) => {
  2594. $c(modalTabGroupTabsCls).find('> button').each((_, tab) => {
  2595. const $tab = jq(tab);
  2596. if ($tab.attr('data-tab') === tabName) {
  2597. $tab.addClass(modalTabGroupActiveCls);
  2598. } else {
  2599. $tab.removeClass(modalTabGroupActiveCls);
  2600. }
  2601. });
  2602. $c(modalTabGroupContentCls).each((_, tab) => {
  2603. const $tab = jq(tab);
  2604. if ($tab.attr('data-tab') === tabName) {
  2605. $tab.show();
  2606. } else {
  2607. $tab.hide();
  2608. }
  2609. });
  2610.  
  2611. // Save the active tab to config
  2612. const config = loadConfigOrDefault();
  2613. saveConfig({
  2614. ...config,
  2615. activeSettingsTab: tabName
  2616. });
  2617. };
  2618.  
  2619. const createTabContent = (tabName, tabLabel) => {
  2620. const $tabButton = jq('<button/>').text(tabLabel).attr('data-tab', tabName).on('click', () => setActiveTab(tabName));
  2621. $tabButtons.append($tabButton);
  2622. const $tabContent = jq('<div/>')
  2623. .addClass(modalTabGroupContentCls)
  2624. .attr('data-tab', tabName);
  2625. getSettingsModalContent().append($tabContent);
  2626. return $tabContent;
  2627. };
  2628.  
  2629. const insertSeparator = () => getSettingsLastTabGroupContent().append('<hr/>');
  2630.  
  2631. // -------------------------------------------------------------------------------------------------------------------
  2632. createTabContent('general', 'General');
  2633.  
  2634. createCheckbox(hideSideMenuCheckboxId, 'Hide Side Menu', saveConfigFromForm);
  2635. createCheckbox(slimLeftMenuCheckboxId, 'Slim Left Menu', saveConfigFromForm);
  2636. createCheckbox(hideHomeWidgetsCheckboxId, 'Hide Home Page Widgets', saveConfigFromForm);
  2637. createCheckbox(hideSideMenuLabelsId, 'Hide Side Menu Labels', saveConfigFromForm);
  2638. createCheckbox(hideDiscoverButtonCheckboxId, 'Hide Discover Button', saveConfigFromForm);
  2639. 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);
  2640. createCheckbox(extraSpaceBellowLastAnswerCheckboxId, 'Add extra space bellow last answer', saveConfigFromForm);
  2641. createSelect(
  2642. replaceIconsInMenuId,
  2643. 'Replace menu icons',
  2644. Object.values(ICON_REPLACEMENT_MODE).map(value => ({ value, label: value })),
  2645. () => {
  2646. saveConfigFromForm();
  2647. replaceIconsInMenu();
  2648. }
  2649. );
  2650. createNumberInput(
  2651. leftMarginOfThreadContentInputId,
  2652. '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)',
  2653. saveConfigFromForm,
  2654. { min: -10, max: 10, step: 0.5 }
  2655. );
  2656.  
  2657.  
  2658. // -------------------------------------------------------------------------------------------------------------------
  2659. createTabContent('model', 'Model');
  2660.  
  2661. createSelect(
  2662. modelLabelStyleSelectId,
  2663. 'Model Label Style',
  2664. Object.values(MODEL_LABEL_STYLE).map(value => ({ value, label: value })),
  2665. saveConfigFromForm
  2666. );
  2667. createSelect(
  2668. modelLabelTextModeSelectId,
  2669. 'Model Label Text',
  2670. Object.values(MODEL_LABEL_TEXT_MODE).map(value => ({ value, label: value })),
  2671. saveConfigFromForm
  2672. );
  2673. createCheckbox(modelLabelOverwriteCyanIconToGrayCheckboxId, 'Overwrite Model Icon: Cyan -> Gray', saveConfigFromForm);
  2674. createSelect(
  2675. modelLabelUseIconForReasoningModelsSelectId,
  2676. 'Use icon for reasoning models',
  2677. Object.values(MODEL_LABEL_ICON_REASONING_MODEL).map(value => ({ value, label: value })),
  2678. saveConfigFromForm
  2679. );
  2680. createColorInput(modelLabelReasoningModelIconColorId, 'Color for reasoning model icon', saveConfigFromForm);
  2681. createSelect(
  2682. modelLabelIconsSelectId,
  2683. 'Model Label Icons',
  2684. Object.values(MODEL_LABEL_ICONS).map(value => ({ value, label: value })),
  2685. saveConfigFromForm
  2686. );
  2687. createSelect(
  2688. customModelPopoverSelectId,
  2689. 'Custom Model Popover (Experimental)',
  2690. Object.values(CUSTOM_MODEL_POPOVER_MODE).map(value => ({ value, label: value })),
  2691. saveConfigFromForm
  2692. );
  2693. createCheckbox(modelLabelRemoveCpuIconCheckboxId, 'Remove CPU icon', saveConfigFromForm);
  2694. createCheckbox(modelLabelLargerIconsCheckboxId, 'Use larger model icons', saveConfigFromForm);
  2695.  
  2696. // -------------------------------------------------------------------------------------------------------------------
  2697. createTabContent('tags', 'Tags');
  2698.  
  2699. createCheckbox(tagsEnabledId, 'Enable Tags', saveConfigFromForm);
  2700.  
  2701. createTextArea(tagsTextAreaId, 'Tags', saveConfigFromForm, tagsHelpText, [
  2702. { icon: 'l:images', tooltip: 'Lucide Icons', url: 'https://lucide.dev/icons' },
  2703. { icon: 'td:image', tooltip: 'TDesign Icons', url: 'https://tdesign.tencent.com/design/icon-en#header-69' }
  2704. ])
  2705. .prop('rows', 12).css('min-width', '700px').prop('wrap', 'off');
  2706.  
  2707. const paletteLegendContainer = jq('<div/>').attr('id', 'palette-legend-container');
  2708. getSettingsLastTabGroupContent().append(paletteLegendContainer);
  2709.  
  2710. const updatePaletteLegend = () => {
  2711. paletteLegendContainer.empty().append(createPaletteLegend(loadConfig()?.tagPalette));
  2712. };
  2713.  
  2714. updatePaletteLegend();
  2715.  
  2716. createSelect(
  2717. tagPaletteSelectId,
  2718. 'Tag color palette',
  2719. Object.keys(TAGS_PALETTES).map(key => ({ value: key, label: key })),
  2720. () => {
  2721. saveConfigFromForm();
  2722. updatePaletteLegend();
  2723. refreshTags();
  2724. }
  2725. );
  2726.  
  2727. createTextArea(
  2728. tagPaletteCustomTextAreaId,
  2729. 'Custom Palette Colors (comma-separated):',
  2730. () => {
  2731. saveConfigFromForm();
  2732. // Update legend and tags only if CUSTOM is the selected palette
  2733. if (getTagPaletteSelect().val() === TAGS_PALETTES.CUSTOM) {
  2734. updatePaletteLegend();
  2735. refreshTags();
  2736. }
  2737. }
  2738. ).prop('rows', 2); // Make it a bit smaller than the main tags text area
  2739.  
  2740. createTagsPreview();
  2741.  
  2742. const FONTS = Object.keys(fontUrls);
  2743.  
  2744. createCheckbox(tagToggleSaveCheckboxId, 'Save toggle-mode tag states', () => {
  2745. const isEnabled = getTagToggleSaveCheckbox().prop('checked');
  2746. // If we're turning off the setting, reset saved toggle states
  2747. if (!isEnabled) {
  2748. const config = loadConfigOrDefault();
  2749. if (config.tagToggledStates && Object.keys(config.tagToggledStates).length > 0) {
  2750. if (confirm('Do you want to clear all saved toggle states?')) {
  2751. const updatedConfig = {
  2752. ...config,
  2753. tagToggledStates: {}
  2754. };
  2755. saveConfig(updatedConfig);
  2756. }
  2757. }
  2758. }
  2759. saveConfigFromForm();
  2760. });
  2761.  
  2762. createCheckbox(toggleModeHooksCheckboxId, 'Toggle mode hooks (experimental)', saveConfigFromForm);
  2763. createCheckbox(tagToggleModeIndicatorsCheckboxId, 'Toggle mode indicators', saveConfigFromForm);
  2764.  
  2765. // Add a reset button for toggle states
  2766. const resetToggleStatesButton = jq('<button>')
  2767. .text('Reset All Toggle States')
  2768. .on('click', () => {
  2769. resetAllToggleStates();
  2770. })
  2771. .css({
  2772. marginLeft: '10px',
  2773. marginBottom: '10px',
  2774. padding: '3px 8px',
  2775. fontSize: '0.9em'
  2776. });
  2777. getSettingsLastTabGroupContent().append(resetToggleStatesButton);
  2778.  
  2779. createSelect(
  2780. tagFontSelectId,
  2781. 'Tag font',
  2782. FONTS.map(font => ({ value: font, label: font })),
  2783. () => {
  2784. saveConfigFromForm();
  2785. loadFont(loadConfigOrDefault().tagFont);
  2786. refreshTags({ force: true });
  2787. }
  2788. );
  2789. createColorInput(tagColorPickerId, 'Custom color - copy field for tag to clipboard', () => {
  2790. const color = getTagColorPicker().val();
  2791. debugLog('color', color);
  2792. copyTextToClipboard(genColorPart(color));
  2793. });
  2794. const saveConfigFromFormAndForceRefreshTags = () => {
  2795. saveConfigFromForm();
  2796. refreshTags({ force: true });
  2797. };
  2798.  
  2799. createCheckbox(tagBoldCheckboxId, 'Bold text', saveConfigFromFormAndForceRefreshTags);
  2800. createCheckbox(tagItalicCheckboxId, 'Italic text', saveConfigFromFormAndForceRefreshTags);
  2801.  
  2802. createNumberInput(
  2803. tagFontSizeInputId,
  2804. 'Font size',
  2805. saveConfigFromFormAndForceRefreshTags,
  2806. { min: 4, max: 64 }
  2807. );
  2808.  
  2809. createNumberInput(
  2810. tagIconSizeInputId,
  2811. 'Icon size',
  2812. saveConfigFromFormAndForceRefreshTags,
  2813. { min: 4, max: 64 }
  2814. );
  2815.  
  2816. createNumberInput(
  2817. tagRoundnessInputId,
  2818. 'Tag Roundness (px)',
  2819. saveConfigFromFormAndForceRefreshTags,
  2820. { min: 0, max: 32 }
  2821. );
  2822.  
  2823. createNumberInput(
  2824. tagTextYOffsetInputId,
  2825. 'Text Y offset',
  2826. saveConfigFromFormAndForceRefreshTags,
  2827. { step: 1, min: -50, max: 50 }
  2828. );
  2829.  
  2830. createNumberInput(
  2831. tagIconYOffsetInputId,
  2832. 'Icon Y offset',
  2833. saveConfigFromFormAndForceRefreshTags,
  2834. { step: 1, min: -50, max: 50 }
  2835. );
  2836.  
  2837. createCheckbox(tagTweakNoBorderCheckboxId, 'No border', saveConfigFromFormAndForceRefreshTags);
  2838. createCheckbox(tagTweakSlimPaddingCheckboxId, 'Slim padding', saveConfigFromFormAndForceRefreshTags);
  2839. createCheckbox(tagTweakRichBorderColorCheckboxId, 'Rich Border Color', saveConfigFromFormAndForceRefreshTags);
  2840. createCheckbox(tagTweakTextShadowCheckboxId, 'Text shadow', saveConfigFromFormAndForceRefreshTags);
  2841. createNumberInput(
  2842. tagLuminanceThresholdInputId,
  2843. 'Tag Luminance Threshold (determines if tag is light or dark)',
  2844. saveConfigFromFormAndForceRefreshTags,
  2845. { step: 0.01, min: 0, max: 1 }
  2846. );
  2847. createSelect(
  2848. tagHomePageLayoutSelectId,
  2849. 'Tag container layout on home page (requires page refresh)',
  2850. Object.values(TAG_HOME_PAGE_LAYOUT).map(value => ({ value, label: value })),
  2851. saveConfigFromForm
  2852. );
  2853. createNumberInput(
  2854. tagContainerExtraBottomMarginInputId,
  2855. 'Extra bottom margin on home page (em)',
  2856. saveConfigFromFormAndForceRefreshTags,
  2857. { min: 0, max: 10, step: 0.5 }
  2858. );
  2859.  
  2860. const $modelsList = jq('<div/>').text('Model IDs: ');
  2861. const modelIds = PP.modelDescriptors.map(md => md.ppModelId).join(', ');
  2862. $modelsList.append(modelIds);
  2863. getSettingsLastTabGroupContent().append($modelsList);
  2864.  
  2865. // -------------------------------------------------------------------------------------------------------------------
  2866. createTabContent('raw', 'Raw (HTML, CSS, JS)');
  2867.  
  2868. createCheckbox(mainCaptionHtmlEnabledId, 'Enable Main Caption HTML', saveConfigFromForm);
  2869. createTextArea(mainCaptionHtmlTextAreaId, 'Main Caption HTML', saveConfigFromForm)
  2870. .prop('rows', 8).css('min-width', '700px');
  2871.  
  2872. insertSeparator();
  2873.  
  2874. createCheckbox(customWidgetsHtmlEnabledId, 'Enable Custom Widgets HTML', saveConfigFromForm);
  2875. createTextArea(customWidgetsHtmlTextAreaId, 'Custom Widgets HTML', saveConfigFromForm)
  2876. .prop('rows', 8).css('min-width', '700px');
  2877.  
  2878. insertSeparator();
  2879.  
  2880. createCheckbox(customCssEnabledId, 'Enable Custom CSS', saveConfigFromForm);
  2881. createTextArea(customCssTextAreaId, 'Custom CSS', saveConfigFromForm)
  2882. .prop('rows', 8).css('min-width', '700px');
  2883.  
  2884. insertSeparator();
  2885.  
  2886. createCheckbox(customJsEnabledId, 'Enable Custom JavaScript', saveConfigFromForm);
  2887. createTextArea(customJsTextAreaId, 'Custom JS', saveConfigFromForm)
  2888. .prop('rows', 8).css('min-width', '700px');
  2889.  
  2890. // -------------------------------------------------------------------------------------------------------------------
  2891. createTabContent('settings', 'Settings');
  2892.  
  2893. 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.'));
  2894.  
  2895. const buttonsContainer = jq('<div/>').addClass('flex gap-2');
  2896. getSettingsLastTabGroupContent().append(buttonsContainer);
  2897.  
  2898. const createExportButton = () => {
  2899. const exportButton = jq('<button>')
  2900. .text('Export Settings')
  2901. .on('click', () => {
  2902. const settings = JSON.stringify(getSavedStates(), null, 2);
  2903. const blob = new Blob([settings], { type: 'application/json' });
  2904. const date = new Date().toISOString().replace(/[:]/g, '-').replace(/T/g, '--').split('.')[0]; // Format: YYYY-MM-DD--HH-MM-SS
  2905. const filename = `perplexity-helper-settings_${date}.json`;
  2906. const url = URL.createObjectURL(blob);
  2907. const a = document.createElement('a');
  2908. a.href = url;
  2909. a.download = filename;
  2910. document.body.appendChild(a);
  2911. a.click();
  2912. document.body.removeChild(a);
  2913. URL.revokeObjectURL(url);
  2914. });
  2915. buttonsContainer.append(exportButton);
  2916. };
  2917. createExportButton();
  2918.  
  2919. const createImportButton = () => {
  2920. const importButton = jq('<button>')
  2921. .text('Import Settings')
  2922. .on('click', () => {
  2923. const input = jq('<input type="file" accept=".json">');
  2924. input.on('change', async (event) => {
  2925. const file = event.target.files[0];
  2926. if (file) {
  2927. // this is a dangerous operation, so we need to confirm it
  2928. const confirmOverwrite = confirm('This will overwrite your current settings. Do you want to continue?');
  2929. if (confirmOverwrite) {
  2930. const reader = new FileReader();
  2931. reader.onload = (e) => {
  2932. try {
  2933. const settings = JSON.parse(e.target.result);
  2934. saveConfig(settings);
  2935. loadCurrentConfigToSettingsForm();
  2936. refreshTags();
  2937. alert('Settings imported successfully!');
  2938. } catch (error) {
  2939. console.error('Error importing settings:', error);
  2940. alert('Error importing settings. Please check the file format.');
  2941. }
  2942. };
  2943. reader.readAsText(file);
  2944. }
  2945. }
  2946. });
  2947. input.trigger('click');
  2948. });
  2949. buttonsContainer.append(importButton);
  2950. };
  2951. createImportButton();
  2952.  
  2953. // -------------------------------------------------------------------------------------------------------------------
  2954. createTabContent('legacy', 'Legacy');
  2955.  
  2956. createCheckbox(coPilotNewThreadAutoSubmitCheckboxId, 'Auto Submit New Thread With CoPilot', saveConfigFromForm);
  2957. createCheckbox(coPilotRepeatLastAutoSubmitCheckboxId, 'Auto Submit Repeat With CoPilot', saveConfigFromForm);
  2958.  
  2959. // -------------------------------------------------------------------------------------------------------------------
  2960. createTabContent('about', 'About');
  2961. getSettingsLastTabGroupContent().append(jq('<div/>').html(`
  2962. Perplexity Helper is a userscript that adds many quality of life features to Perplexity.<br>
  2963. <br>
  2964. 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>
  2965. Original author: <a href="https://gitlab.com/tiartyos" target="_blank">Tiartyos</a> <span class="opacity-50">(copilot buttons, basic settings)</span>
  2966. `));
  2967.  
  2968. // -------------------------------------------------------------------------------------------------------------------
  2969. createTabContent('debug', 'Debug'); // debug options at the bottom (do NOT add more normal options bellow this!)
  2970.  
  2971. createCheckbox(enableDebugCheckboxId, 'Debug Mode', () => {
  2972. saveConfigFromForm();
  2973. const checked = getEnableDebugCheckbox().prop('checked');
  2974. if (checked) {
  2975. enableDebugMode();
  2976. }
  2977. });
  2978.  
  2979. createCheckbox(enableTagsDebugCheckboxId, 'Debug Tags Mode', () => {
  2980. saveConfigFromForm();
  2981. const checked = getEnableTagsDebugCheckbox().prop('checked');
  2982. if (checked) {
  2983. enableTagsDebugging();
  2984. refreshTags();
  2985. }
  2986. });
  2987.  
  2988. createCheckbox(debugTagsSuppressSubmitCheckboxId, 'Debug: Suppress Submit After Applying Tags', saveConfigFromForm);
  2989.  
  2990. createCheckbox(autoOpenSettingsCheckboxId, 'Automatically open settings after page load', saveConfigFromForm);
  2991.  
  2992. getSettingsLastTabGroupContent().append(`
  2993. <h2>Lobe Icons test</h2>
  2994. <table style="border-collapse: separate; border-spacing: 20px; width: fit-content;">
  2995. <tr>
  2996. <td>Default</td>
  2997. <td><img src="${getLobeIconsUrl('anthropic')}"></td>
  2998. </tr>
  2999. <tr>
  3000. <td>Default (inverted)</td>
  3001. <td><img class="invert" src="${getLobeIconsUrl('anthropic')}"></td>
  3002. </tr>
  3003. </table>
  3004. `);
  3005.  
  3006. // -------------------------------------------------------------------------------------------------------------------
  3007. // Use the saved active tab if available, otherwise default to 'general'
  3008. const config = loadConfigOrDefault();
  3009. setActiveTab(config.activeSettingsTab || defaultConfig.activeSettingsTab);
  3010. loadCurrentConfigToSettingsForm();
  3011. }
  3012.  
  3013. debugLog(jq.fn.jquery);
  3014. const getSavedStates = () => JSON.parse(localStorage.getItem(storageKey));
  3015.  
  3016. const getModal = () => jq("[data-testid='quick-search-modal'] > div");
  3017. const getCopilotToggleButton = textarea => textarea.parent().parent().find('[data-testid="copilot-toggle"]');
  3018. const upperControls = () => jq('svg[data-icon="lock"] ~ div:contains("Share")').nthParent(5).closest('.flex.justify-between:not(.grid-cols-3)');
  3019.  
  3020. const getControlsArea = () => jq('textarea[placeholder="Ask follow-up"]').parent().parent().children().last();
  3021.  
  3022. const getCopilotNewThreadButton = () => jq('#copilot_new_thread');
  3023. const getCopilotRepeatLastButton = () => jq('#copilot_repeat_last');
  3024. const getSelectAllButton = () => jq('#perplexity_helper_select_all');
  3025. const getSelectAllAndSubmitButton = () => jq('#perplexity_helper_select_all_and_submit');
  3026. const getCopyPlaceholder = () => jq('#perplexity_helper_copy_placeholder');
  3027. const getCopyAndFillInPlaceholder = () => jq('#perplexity_helper_copy_placeholder_and_fill_in');
  3028. const getTopSettingsButtonEl = () => $i(topSettingsButtonId);
  3029. const getLeftSettingsButtonEl = () => $i(leftSettingsButtonId);
  3030. const getSettingsModalContent = () => getPerplexityHelperModal().find(`.modal-content`);
  3031. const getSettingsLastTabGroupContent = () => getSettingsModalContent().find(`.${modalTabGroupContentCls}`).last();
  3032.  
  3033. const getSubmitBtn0 = () => jq('svg[data-icon="arrow-up"]').last().parent().parent();
  3034. const getSubmitBtn1 = () => jq('svg[data-icon="arrow-right"]').last().parent().parent();
  3035. const getSubmitBtn2 = () => jq('svg[data-icon="code-fork"]').last().parent().parent();
  3036.  
  3037. const isStandardControlsAreaFc = () => !getControlsArea().hasClass('bottom-0');
  3038. const getCurrentControlsArea = () => isStandardControlsAreaFc() ? getControlsArea() : getControlsArea().find('.bottom-0');
  3039.  
  3040. const getDashedCheckboxButton = () => jq('svg[data-icon="square-dashed"]').parent().parent();
  3041. const getStarSVG = () => jq('svg[data-icon="star-christmas"]');
  3042. const getSpecifyQuestionBox = () => jq('svg[data-icon="star-christmas"]').parent().parent().parent().last();
  3043.  
  3044. const getNumberOfDashedSVGs = () => getSpecifyQuestionBox().find('svg[data-icon="square-dashed"]').length;
  3045. const getSpecifyQuestionControlsWrapper = () => getSpecifyQuestionBox().find('button:contains("Continue")').parent();
  3046. const getCopiedModal = () => jq('#copied-modal');
  3047. const getCopiedModal2 = () => jq('#copied-modal-2');
  3048. const getCopyPlaceholderInput = () => getSpecifyQuestionBox().find('textarea');
  3049.  
  3050. const getSubmitButton0or2 = () => getSubmitBtn0().length < 1 ? getSubmitBtn2() : getSubmitBtn0();
  3051.  
  3052. const questionBoxWithPlaceholderExists = () => getSpecifyQuestionBox().find('textarea')?.attr('placeholder')?.length > 0 ?? false;
  3053.  
  3054. // TODO: no longer used? was this for agentic questions?
  3055. const selectAllCheckboxes = () => {
  3056. const currentCheckboxes = getDashedCheckboxButton();
  3057. debugLog('checkboxes', currentCheckboxes);
  3058.  
  3059. const removeLastObject = (arr) => {
  3060. if (!_.isEmpty(arr)) {
  3061. debugLog('arr', arr);
  3062. const newArr = _.dropRight(arr, 1);
  3063. debugLog("newArr", newArr);
  3064. getDashedCheckboxButton().last().click();
  3065.  
  3066. return setTimeout(() => {
  3067. removeLastObject(newArr);
  3068. }, 1);
  3069.  
  3070. }
  3071. };
  3072.  
  3073. removeLastObject(currentCheckboxes);
  3074. };
  3075.  
  3076. const isCopilotOn = (el) => el.hasClass('text-super');
  3077.  
  3078. const toggleBtnDot = (btnDot, value) => {
  3079. debugLog(' toggleBtnDot btnDot', btnDot);
  3080.  
  3081. const btnDotInner = btnDot.find('.rounded-full');
  3082.  
  3083. debugLog('btnDotInner', btnDotInner);
  3084.  
  3085. if (!btnDotInner.hasClass('bg-super') && value === true) {
  3086. btnDot.click();
  3087. }
  3088. };
  3089.  
  3090. const checkForCopilotToggleState = (timer, checkCondition, submitWhenTrue, submitButtonVersion) => {
  3091. debugLog("checkForCopilotToggleState run", timer, checkCondition(), submitWhenTrue, submitButtonVersion);
  3092. if (checkCondition()) {
  3093. clearInterval(timer);
  3094. debugLog("checkForCopilotToggleState condition met, interval cleared");
  3095. const submitBtn = submitButtonVersion === 0 ? getSubmitButton0or2() : getSubmitBtn1();
  3096. debugLog('submitBtn', submitBtn);
  3097. if (submitWhenTrue) {
  3098. submitBtn.click();
  3099. }
  3100. }
  3101. };
  3102.  
  3103. const openNewThreadModal = (lastQuery) => {
  3104. debugLog('openNewThreadModal', lastQuery);
  3105. const newThreadText = jq(".sticky div").filter(function () {
  3106. return /^New Thread$/i.test(jq(this).text());
  3107. });
  3108. if (!newThreadText.length) {
  3109. debugLog('newThreadText.length should be 1', newThreadText.length);
  3110. return;
  3111. }
  3112. debugLog('newThreadText', newThreadText);
  3113.  
  3114. newThreadText.click();
  3115. setTimeout(() => {
  3116. debugLog('newThreadText.click()');
  3117. const modal = getModal();
  3118.  
  3119. if (modal.length > 0) {
  3120. const textArea = modal.find('textarea');
  3121. if (textArea.length !== 1) debugLog('textArea.length should be 1', textArea.length);
  3122.  
  3123. const newTextArea = textArea.last();
  3124. const textareaElement = newTextArea[0];
  3125. debugLog('textareaElement', textareaElement);
  3126. changeValueUsingEvent(textareaElement, lastQuery);
  3127.  
  3128. const copilotButton = getCopilotToggleButton(newTextArea);
  3129.  
  3130. toggleBtnDot(copilotButton, true);
  3131. const isCopilotOnBtn = () => isCopilotOn(copilotButton);
  3132.  
  3133. const coPilotNewThreadAutoSubmit =
  3134. getSavedStates()
  3135. ? getSavedStates().coPilotNewThreadAutoSubmit
  3136. : getCoPilotNewThreadAutoSubmitCheckbox().prop('checked');
  3137.  
  3138. const copilotCheck = () => {
  3139. const ctx = { timer: null };
  3140. ctx.timer = setInterval(() => checkForCopilotToggleState(ctx.timer, isCopilotOnBtn, coPilotNewThreadAutoSubmit, 1), 500);
  3141. };
  3142.  
  3143. copilotCheck();
  3144. } else {
  3145. debugLog('else of modal.length > 0');
  3146. }
  3147. },
  3148. 2000);
  3149. };
  3150.  
  3151. const getLastQuery = () => {
  3152. // wrapper around prompt + response
  3153. const lastQueryBox = jq('svg[data-icon="repeat"]').last().nthParent(7);
  3154. if (lastQueryBox.length === 0) {
  3155. debugLog('lastQueryBox not found');
  3156. }
  3157.  
  3158. const wasCopilotUsed = lastQueryBox.find('svg[data-icon="star-christmas"]').length > 0;
  3159. const lastQueryBoxText = lastQueryBox.find('.whitespace-pre-line').text();
  3160.  
  3161. debugLog('[getLastQuery]', { lastQueryBox, wasCopilotUsed, lastQueryBoxText });
  3162. return lastQueryBoxText ?? null;
  3163. };
  3164.  
  3165. const saveConfigFromForm = () => {
  3166. const newConfig = {
  3167. ...loadConfigOrDefault(),
  3168. coPilotNewThreadAutoSubmit: getCoPilotNewThreadAutoSubmitCheckbox().prop('checked'),
  3169. coPilotRepeatLastAutoSubmit: getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked'),
  3170. hideSideMenu: getHideSideMenuCheckbox().prop('checked'),
  3171. slimLeftMenu: getSlimLeftMenuCheckbox().prop('checked'),
  3172. hideSideMenuLabels: getHideSideMenuLabels().prop('checked'),
  3173. tagsEnabled: getTagsEnabledCheckbox().prop('checked'),
  3174. tagsText: getTagsTextArea().val(),
  3175. tagPalette: getTagPaletteSelect().val(),
  3176. tagPaletteCustom: getTagPaletteCustomTextArea().val().split(',').map(s => s.trim()),
  3177. tagFont: getTagFontSelect().val(),
  3178. tagHomePageLayout: getTagHomePageLayoutSelect().val(),
  3179. tagContainerExtraBottomMargin: parseFloat(getTagContainerExtraBottomMarginInput().val()),
  3180. tagLuminanceThreshold: parseFloat(getTagLuminanceThresholdInput().val()),
  3181. tagBold: getTagBoldCheckbox().prop('checked'),
  3182. tagItalic: getTagItalicCheckbox().prop('checked'),
  3183. tagFontSize: parseFloat(getTagFontSizeInput().val()),
  3184. tagIconSize: parseFloat(getTagIconSizeInput().val()),
  3185. tagRoundness: parseFloat(getTagRoundnessInput().val()),
  3186. tagTextYOffset: parseFloat(getTagTextYOffsetInput().val()),
  3187. tagIconYOffset: parseFloat(getTagIconYOffsetInput().val()),
  3188. tagToggleSave: getTagToggleSaveCheckbox().prop('checked'),
  3189. toggleModeHooks: getToggleModeHooksCheckbox().prop('checked'),
  3190. tagToggleModeIndicators: getTagToggleModeIndicatorsCheckbox().prop('checked'),
  3191. debugMode: getEnableDebugCheckbox().prop('checked'),
  3192. debugTagsMode: getEnableTagsDebugCheckbox().prop('checked'),
  3193. debugTagsSuppressSubmit: getDebugTagsSuppressSubmitCheckbox().prop('checked'),
  3194. autoOpenSettings: getAutoOpenSettingsCheckbox().prop('checked'),
  3195. replaceIconsInMenu: getReplaceIconsInMenu().val(),
  3196. hideHomeWidgets: getHideHomeWidgetsCheckbox().prop('checked'),
  3197. hideDiscoverButton: getHideDiscoverButtonCheckbox().prop('checked'),
  3198. fixImageGenerationOverlay: getFixImageGenerationOverlayCheckbox().prop('checked'),
  3199. extraSpaceBellowLastAnswer: getExtraSpaceBellowLastAnswerCheckbox().prop('checked'),
  3200. modelLabelTextMode: getModelLabelTextModeSelect().val(),
  3201. modelLabelStyle: getModelLabelStyleSelect().val(),
  3202. modelLabelOverwriteCyanIconToGray: getModelLabelOverwriteCyanIconToGrayCheckbox().prop('checked'),
  3203. modelLabelUseIconForReasoningModels: getModelLabelUseIconForReasoningModelsSelect().val(),
  3204. modelLabelReasoningModelIconColor: getModelLabelReasoningModelIconColor().val(),
  3205. modelLabelRemoveCpuIcon: getModelLabelRemoveCpuIconCheckbox().prop('checked'),
  3206. modelLabelLargerIcons: getModelLabelLargerIconsCheckbox().prop('checked'),
  3207. modelLabelIcons: getModelLabelIconsSelect().val(),
  3208. customModelPopover: getCustomModelPopoverSelect().val(),
  3209. mainCaptionHtml: getMainCaptionHtmlTextArea().val(),
  3210. mainCaptionHtmlEnabled: getMainCaptionHtmlEnabledCheckbox().prop('checked'),
  3211. customJs: getCustomJsTextArea().val(),
  3212. customJsEnabled: getCustomJsEnabledCheckbox().prop('checked'),
  3213. customCss: getCustomCssTextArea().val(),
  3214. customCssEnabled: getCustomCssEnabledCheckbox().prop('checked'),
  3215. customWidgetsHtml: getCustomWidgetsHtmlTextArea().val(),
  3216. customWidgetsHtmlEnabled: getCustomWidgetsHtmlEnabledCheckbox().prop('checked'),
  3217. leftMarginOfThreadContent: getLeftMarginOfThreadContentInput().val() === "" ? null : parseFloat(getLeftMarginOfThreadContentInput().val()),
  3218. };
  3219. saveConfig(newConfig);
  3220. };
  3221.  
  3222. const showPerplexityHelperSettingsModal = () => {
  3223. loadCurrentConfigToSettingsForm();
  3224. getPerplexityHelperModal().show().css('display', 'flex');
  3225. };
  3226.  
  3227. const hidePerplexityHelperSettingsModal = () => {
  3228. getPerplexityHelperModal().hide();
  3229. };
  3230.  
  3231. const handleTopSettingsButtonInsertion = () => {
  3232. const copilotHelperSettings = getTopSettingsButtonEl();
  3233. // TODO: no longer works
  3234. // debugLog('upperControls().length > 0', upperControls().length, 'copilotHelperSettings.length', copilotHelperSettings.length, 'upperControls().children().length', upperControls().children().length);
  3235. if (upperControls().length > 0 && copilotHelperSettings.length < 1 && upperControls().children().length >= 1) {
  3236. debugLog('inserting settings button');
  3237. upperControls().children().eq(0).children().eq(0).append(upperButton(topSettingsButtonId, cogIco, 'Perplexity Helper Settings'));
  3238. }
  3239. };
  3240.  
  3241. const handleTopSettingsButtonSetup = () => {
  3242. const settingsButtonEl = getTopSettingsButtonEl();
  3243.  
  3244. if (settingsButtonEl.length === 1 && !settingsButtonEl.attr('data-has-custom-click-event')) {
  3245. debugLog('handleTopSettingsButtonSetup: setting up the button');
  3246. if (settingsButtonEl.length === 0) {
  3247. debugLog('handleTopSettingsButtonSetup: settingsButtonEl.length === 0');
  3248. }
  3249.  
  3250. settingsButtonEl.on("click", () => {
  3251. debugLog('perplexity_helper_settings open click');
  3252. showPerplexityHelperSettingsModal();
  3253. });
  3254.  
  3255. settingsButtonEl.attr('data-has-custom-click-event', true);
  3256. }
  3257. };
  3258.  
  3259. const applySideMenuHiding = () => {
  3260. const config = loadConfigOrDefault();
  3261. if (!config.hideSideMenu) return;
  3262. const $sideMenu = PP.getLeftPanel();
  3263. if ($sideMenu.hasClass(sideMenuHiddenCls)) return;
  3264. $sideMenu.addClass(sideMenuHiddenCls);
  3265. 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 });
  3266. };
  3267.  
  3268. const handleModalCreation = () => {
  3269. if (getPerplexityHelperModal().length > 0) return;
  3270. debugLog('handleModalCreation: creating modal');
  3271. jq("body").append(modalHTML);
  3272.  
  3273. getPerplexityHelperModal().find('.close').on('click', () => {
  3274. debugLog('perplexity_helper_settings close click');
  3275. hidePerplexityHelperSettingsModal();
  3276. });
  3277.  
  3278. // Setup title animation
  3279. setTimeout(() => {
  3280. const $titleEl = getPerplexityHelperModal().find(`.${modalSettingsTitleCls}`);
  3281. if ($titleEl.length) {
  3282. const text = $titleEl.text();
  3283. const wrappedText = text
  3284. .split('')
  3285. .map((char, i) => {
  3286. if (i === 0 || i === 11) { // P and H positions
  3287. return `<span class="animate-letter" data-letter="${char}">${char}</span>`;
  3288. }
  3289. return char;
  3290. })
  3291. .join('');
  3292.  
  3293. $titleEl.html(wrappedText);
  3294.  
  3295. $titleEl.on('click', () => {
  3296. const $firstLetter = $titleEl.find('.animate-letter').eq(0);
  3297. const $secondLetter = $titleEl.find('.animate-letter').eq(1);
  3298.  
  3299. // Staggered animation
  3300. $firstLetter.addClass('active');
  3301. setTimeout(() => {
  3302. $firstLetter.removeClass('active');
  3303. $secondLetter.addClass('active');
  3304. setTimeout(() => {
  3305. $secondLetter.removeClass('active');
  3306. }, 500);
  3307. }, 250);
  3308. });
  3309. }
  3310. }, 500);
  3311. };
  3312.  
  3313. const lucideIconMappings = {
  3314. LUCIDE1: leftPanelIconMappingsToLucide1,
  3315. LUCIDE2: leftPanelIconMappingsToLucide2,
  3316. };
  3317.  
  3318. const findKeyByValue = (obj, value) =>
  3319. Object.keys(obj).find(key => obj[key] === value);
  3320.  
  3321. const SUPPORTED_ICON_REPLACEMENT_MODES = [
  3322. ICON_REPLACEMENT_MODE.LUCIDE1,
  3323. ICON_REPLACEMENT_MODE.LUCIDE2,
  3324. ICON_REPLACEMENT_MODE.LUCIDE3,
  3325. ICON_REPLACEMENT_MODE.TDESIGN1,
  3326. ICON_REPLACEMENT_MODE.TDESIGN2,
  3327. ICON_REPLACEMENT_MODE.TDESIGN3,
  3328. ];
  3329.  
  3330. const replaceIconsInMenu = () => {
  3331. const config = loadConfigOrDefault();
  3332. const replacementMode = findKeyByValue(ICON_REPLACEMENT_MODE, config.replaceIconsInMenu);
  3333.  
  3334. if (SUPPORTED_ICON_REPLACEMENT_MODES.includes(config.replaceIconsInMenu)) {
  3335. const processedAttr = `data-${pplxHelperTag}-processed`;
  3336. const iconMapping = iconMappings[replacementMode];
  3337. if (!iconMapping) {
  3338. console.error(logPrefix, '[replaceIconsInMenu] iconMapping not found', { config, iconMappings });
  3339. return;
  3340. }
  3341.  
  3342. const $iconButtons = PP.getIconsInLeftPanel().find('a:has(> div.grid > svg)');
  3343. // debugLog('[replaceIconsInMenu] svgEls', svgEls);
  3344. $iconButtons.each((idx, rawIconButton) => {
  3345. const $iconButton = jq(rawIconButton);
  3346. const $svg = $iconButton.find('svg');
  3347. const processed = $iconButton.attr(processedAttr);
  3348. if (processed) return;
  3349. if ($iconButton.attr('id') === leftSettingsButtonId) return;
  3350.  
  3351. const iconName = pipe($iconButton.attr('href'))(
  3352. dropStr(1),
  3353. dropRightStr(1),
  3354. ) || 'search';
  3355. debugLog('[replaceIconsInMenu] iconName', iconName);
  3356. const replacementIconName = iconMapping[iconName];
  3357. debugLog('[replaceIconsInMenu] replacementIconName', replacementIconName);
  3358.  
  3359. $iconButton.attr(processedAttr, true);
  3360.  
  3361. if (replacementIconName) {
  3362. const isTDesign = config.replaceIconsInMenu.startsWith('TDesign');
  3363. const newIconUrl = (isTDesign ? getTDesignIconUrl : getLucideIconUrl)(replacementIconName);
  3364.  
  3365. debugLog('[replaceIconsInMenu] replacing icon', { iconName, replacementIconName, $svg, newIconUrl });
  3366. $svg.hide();
  3367. const newIconEl = jq('<img>')
  3368. .attr('src', newIconUrl)
  3369. .addClass('invert opacity-50')
  3370. .addClass('relative duration-150 [grid-area:1/-1] group-hover:scale-110 text-text-200')
  3371. ;
  3372. if (isTDesign) newIconEl.addClass('h-6');
  3373. $svg.parent().addClass(lucideIconParentCls);
  3374. $svg.after(newIconEl);
  3375. } else {
  3376. if (!['plus', 'thread'].includes(iconName)) {
  3377. console.error('[replaceIconsInMenu] no replacement icon found', { iconName, replacementIconName });
  3378. }
  3379. }
  3380. });
  3381. }
  3382. };
  3383.  
  3384. const createSidebarButton = (options) => {
  3385. const { svgHtml, label, testId, href } = options;
  3386. return jq('<a>', {
  3387. 'data-testid': testId,
  3388. 'class': 'p-sm group flex w-full flex-col items-center justify-center gap-0.5',
  3389. 'href': href ?? '#',
  3390. }).append(
  3391. jq('<div>', {
  3392. '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'
  3393. }).append(
  3394. jq('<div>', {
  3395. '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'
  3396. }),
  3397. jq(svgHtml).addClass('relative duration-150 [grid-area:1/-1] group-hover:scale-110 text-text-200'),
  3398. ),
  3399. jq('<div>', {
  3400. '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',
  3401. 'text': label ?? 'MISSING LABEL'
  3402. })
  3403. );
  3404. };
  3405.  
  3406. const handleLeftSettingsButtonSetup = () => {
  3407. const existingLeftSettingsButton = getLeftSettingsButtonEl();
  3408. if (existingLeftSettingsButton.length === 1) {
  3409. // const wrapper = existingLeftSettingsButton.parent();
  3410. // if (!wrapper.is(':last-child')) {
  3411. // wrapper.appendTo(wrapper.parent());
  3412. // }
  3413. return;
  3414. }
  3415.  
  3416. const $leftPanel = PP.getIconsInLeftPanel();
  3417.  
  3418. if ($leftPanel.length === 0) {
  3419. debugLog('handleLeftSettingsButtonSetup: leftPanel not found');
  3420. }
  3421.  
  3422. const $sidebarButton = createSidebarButton({
  3423. svgHtml: cogIco,
  3424. label: 'Perplexity Helper',
  3425. testId: 'perplexity-helper-settings',
  3426. href: '#',
  3427. })
  3428. .attr('id', leftSettingsButtonId)
  3429. .on('click', () => {
  3430. debugLog('left settings button clicked');
  3431. if (!PP.isBreakpoint('md')) {
  3432. PP.getLeftPanel().hide();
  3433. }
  3434. showPerplexityHelperSettingsModal();
  3435. });
  3436.  
  3437. $leftPanel.append($sidebarButton);
  3438. };
  3439.  
  3440. const handleSlimLeftMenu = () => {
  3441. const config = loadConfigOrDefault();
  3442. if (!config.slimLeftMenu) return;
  3443.  
  3444. const $leftPanel = PP.getLeftPanel();
  3445. if ($leftPanel.length === 0) {
  3446. // debugLog('handleSlimLeftMenu: leftPanel not found');
  3447. }
  3448.  
  3449. $leftPanel.addClass(leftPanelSlimCls);
  3450. $leftPanel.find('.py-md').css('width', '45px');
  3451. };
  3452.  
  3453. const handleHideHomeWidgets = () => {
  3454. const config = loadConfigOrDefault();
  3455. if (!config.hideHomeWidgets) return;
  3456.  
  3457. const homeWidgets = PP.getHomeWidgets();
  3458. if (homeWidgets.length === 0) {
  3459. debugLog('handleHideHomeWidgets: homeWidgets not found');
  3460. return;
  3461. }
  3462. if (homeWidgets.length > 1) {
  3463. console.warn(logPrefix, '[handleHideHomeWidgets] too many homeWidgets found', homeWidgets);
  3464. }
  3465.  
  3466. homeWidgets.hide();
  3467. };
  3468.  
  3469. const handleFixImageGenerationOverlay = () => {
  3470. const config = loadConfigOrDefault();
  3471. if (!config.fixImageGenerationOverlay) return;
  3472.  
  3473. const imageGenerationOverlay = PP.getImageGenerationOverlay();
  3474. if (imageGenerationOverlay.length === 0) {
  3475. // debugLog('handleFixImageGenerationOverlay: imageGenerationOverlay not found');
  3476. return;
  3477. }
  3478.  
  3479. // only if wrench button is cyan (we are in custom prompt)
  3480. if (!imageGenerationOverlay.find('button').hasClass('bg-super')) return;
  3481.  
  3482. const transform = imageGenerationOverlay.css('transform');
  3483. if (!transform) return;
  3484.  
  3485. // Handle both matrix and translate formats
  3486. const matrixMatch = transform.match(/matrix\(.*,\s*([\d.]+),\s*([\d.]+)\)/);
  3487. const translateMatch = transform.match(/translate\(([\d.]+)px(?:,\s*([\d.]+)px)?\)/);
  3488.  
  3489. const currentX = matrixMatch
  3490. ? matrixMatch[1] // Matrix format: 5th value is X translation
  3491. : translateMatch?.[1] || 0; // Translate format: first value
  3492.  
  3493. debugLog('[handleFixImageGenerationOverlay] currentX', currentX, 'transform', transform);
  3494. imageGenerationOverlay.css({
  3495. transform: `translate(${currentX}px, 0px)`
  3496. });
  3497. };
  3498.  
  3499. const handleExtraSpaceBellowLastAnswer = () => {
  3500. const config = loadConfigOrDefault();
  3501. if (!config.extraSpaceBellowLastAnswer) return;
  3502. jq('body')
  3503. .find(`.erp-sidecar\\:h-fit .md\\:pt-md.isolate > .max-w-threadContentWidth`)
  3504. .last()
  3505. .css({
  3506. // backgroundColor: 'magenta',
  3507. paddingBottom: '15em',
  3508. })
  3509. ;
  3510. };
  3511.  
  3512. const handleSearchPage = () => {
  3513. const controlsArea = getCurrentControlsArea();
  3514. controlsArea.addClass(controlsAreaCls);
  3515. controlsArea.parent().find('textarea').first().addClass(textAreaCls);
  3516. controlsArea.addClass(roundedMD);
  3517. controlsArea.parent().addClass(roundedMD);
  3518.  
  3519.  
  3520. if (controlsArea.length === 0) {
  3521. debugLog('controlsArea not found', {
  3522. controlsArea,
  3523. currentControlsArea: getCurrentControlsArea(),
  3524. isStandardControlsAreaFc: isStandardControlsAreaFc()
  3525. });
  3526. }
  3527.  
  3528. const lastQueryBoxText = getLastQuery();
  3529.  
  3530. const mainTextArea = isStandardControlsAreaFc() ? controlsArea.prev().prev() : controlsArea.parent().prev();
  3531.  
  3532. if (mainTextArea.length === 0) {
  3533. debugLog('mainTextArea not found', mainTextArea);
  3534. }
  3535.  
  3536.  
  3537. debugLog('lastQueryBoxText', { lastQueryBoxText });
  3538. if (lastQueryBoxText) {
  3539. const copilotNewThread = getCopilotNewThreadButton();
  3540. const copilotRepeatLast = getCopilotRepeatLastButton();
  3541.  
  3542. if (controlsArea.length > 0 && copilotNewThread.length < 1) {
  3543. controlsArea.append(button('copilot_new_thread', robotIco, "Starts new thread for with last query text and Copilot ON", standardButtonCls));
  3544. }
  3545.  
  3546. // Due to updates in Perplexity, this is unnecessary for now
  3547. // if (controlsArea.length > 0 && copilotRepeatLast.length < 1) {
  3548. // controlsArea.append(button('copilot_repeat_last', robotRepeatIco, "Repeats last query with Copilot ON"));
  3549. // }
  3550.  
  3551. if (!copilotNewThread.attr('data-has-custom-click-event')) {
  3552. copilotNewThread.on("click", function () {
  3553. debugLog('copilotNewThread Button clicked!');
  3554. openNewThreadModal(getLastQuery());
  3555. });
  3556. copilotNewThread.attr('data-has-custom-click-event', true);
  3557. }
  3558.  
  3559. if (!copilotRepeatLast.attr('data-has-custom-click-event')) {
  3560. copilotRepeatLast.on("click", function () {
  3561. const controlsArea = getCurrentControlsArea();
  3562. const textAreaElement = controlsArea.parent().find('textarea')[0];
  3563.  
  3564. const coPilotRepeatLastAutoSubmit =
  3565. getSavedStates()
  3566. ? getSavedStates().coPilotRepeatLastAutoSubmit
  3567. : getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked');
  3568.  
  3569. debugLog('coPilotRepeatLastAutoSubmit', coPilotRepeatLastAutoSubmit);
  3570. changeValueUsingEvent(textAreaElement, getLastQuery());
  3571. const copilotToggleButton = getCopilotToggleButton(mainTextArea);
  3572. debugLog('mainTextArea', mainTextArea);
  3573. debugLog('copilotToggleButton', copilotToggleButton);
  3574.  
  3575. toggleBtnDot(copilotToggleButton, true);
  3576. const isCopilotOnBtn = () => isCopilotOn(copilotToggleButton);
  3577.  
  3578. const copilotCheck = () => {
  3579. const ctx = { timer: null };
  3580. ctx.timer = setInterval(() => checkForCopilotToggleState(ctx.timer, isCopilotOnBtn, coPilotRepeatLastAutoSubmit, 0), 500);
  3581. };
  3582.  
  3583. copilotCheck();
  3584. debugLog('copilot_repeat_last Button clicked!');
  3585. });
  3586. copilotRepeatLast.attr('data-has-custom-click-event', true);
  3587. }
  3588. }
  3589.  
  3590. if (getNumberOfDashedSVGs() > 0 && getNumberOfDashedSVGs() === getDashedCheckboxButton().length
  3591. && getSelectAllButton().length < 1 && getSelectAllAndSubmitButton().length < 1) {
  3592. debugLog('getNumberOfDashedSVGs() === getNumberOfDashedSVGs()', getNumberOfDashedSVGs());
  3593. debugLog('getSpecifyQuestionBox', getSpecifyQuestionBox());
  3594.  
  3595. const specifyQuestionControlsWrapper = getSpecifyQuestionControlsWrapper();
  3596. debugLog('specifyQuestionControlsWrapper', specifyQuestionControlsWrapper);
  3597. const selectAllButton = textButton('perplexity_helper_select_all', 'Select all', 'Selects all options');
  3598. const selectAllAndSubmitButton = textButton('perplexity_helper_select_all_and_submit', 'Select all & submit', 'Selects all options and submits');
  3599.  
  3600. specifyQuestionControlsWrapper.append(selectAllButton);
  3601. specifyQuestionControlsWrapper.append(selectAllAndSubmitButton);
  3602.  
  3603. getSelectAllButton().on("click", function () {
  3604. selectAllCheckboxes();
  3605. });
  3606.  
  3607. getSelectAllAndSubmitButton().on("click", function () {
  3608. selectAllCheckboxes();
  3609. setTimeout(() => {
  3610. getSpecifyQuestionControlsWrapper().find('button:contains("Continue")').click();
  3611. }, 200);
  3612. });
  3613. }
  3614.  
  3615. const constructClipBoard = (buttonId, buttonGetter, modalGetter, copiedModalId, elementGetter) => {
  3616. const placeholderValue = getSpecifyQuestionBox().find('textarea').attr('placeholder');
  3617.  
  3618. const clipboardInstance = new ClipboardJS(`#${buttonId}`, {
  3619. text: () => placeholderValue
  3620. });
  3621.  
  3622. const copiedModal = `<span id="${copiedModalId}">Copied!</span>`;
  3623. debugLog('copiedModalId', copiedModalId);
  3624. debugLog('copiedModal', copiedModal);
  3625.  
  3626. jq('main').append(copiedModal);
  3627.  
  3628. clipboardInstance.on('success', _ => {
  3629. var buttonPosition = buttonGetter().position();
  3630. jq(`#${copiedModalId}`).css({
  3631. top: buttonPosition.top - 30,
  3632. left: buttonPosition.left + 50
  3633. }).show();
  3634.  
  3635. if (elementGetter !== undefined) {
  3636. changeValueUsingEvent(elementGetter()[0], placeholderValue);
  3637. }
  3638.  
  3639. setTimeout(() => {
  3640. modalGetter().hide();
  3641. }, 5000);
  3642. });
  3643. };
  3644.  
  3645. if (questionBoxWithPlaceholderExists() && getCopyPlaceholder().length < 1) {
  3646. const copyPlaceholder = textButton('perplexity_helper_copy_placeholder', 'Copy placeholder', 'Copies placeholder value');
  3647. const copyPlaceholderAndFillIn = textButton('perplexity_helper_copy_placeholder_and_fill_in', 'Copy placeholder and fill in',
  3648. 'Copies placeholder value and fills in input');
  3649.  
  3650. const specifyQuestionControlsWrapper = getSpecifyQuestionControlsWrapper();
  3651.  
  3652. specifyQuestionControlsWrapper.append(copyPlaceholder);
  3653. specifyQuestionControlsWrapper.append(copyPlaceholderAndFillIn);
  3654.  
  3655. constructClipBoard('perplexity_helper_copy_placeholder', getCopyPlaceholder, getCopiedModal, 'copied-modal');
  3656. constructClipBoard('perplexity_helper_copy_placeholder_and_fill_in', getCopyAndFillInPlaceholder, getCopiedModal2, 'copied-modal-2', getCopyPlaceholderInput);
  3657. }
  3658. };
  3659.  
  3660. const getLabelFromModelDescription = modelLabelStyle => modelLabelFromAriaLabel => modelDescription => {
  3661. if (!modelDescription) return modelLabelFromAriaLabel;
  3662. switch (modelLabelStyle) {
  3663. case MODEL_LABEL_TEXT_MODE.OFF:
  3664. return '';
  3665. case MODEL_LABEL_TEXT_MODE.FULL_NAME:
  3666. return modelDescription.nameEn;
  3667. case MODEL_LABEL_TEXT_MODE.SHORT_NAME:
  3668. return modelDescription.nameEnShort ?? modelDescription.nameEn;
  3669. case MODEL_LABEL_TEXT_MODE.PP_MODEL_ID:
  3670. return modelDescription.ppModelId;
  3671. case MODEL_LABEL_TEXT_MODE.OWN_NAME_VERSION_SHORT:
  3672. const nameText = modelDescription.ownNameEn ?? modelDescription.nameEn;
  3673. const versionTextRaw = modelDescription.ownVersionEnShort ?? modelDescription.ownVersionEn;
  3674. const versionText = versionTextRaw?.replace(/ P$/, ' Pro'); // HACK: Gemini 2.5 Pro
  3675. return [nameText, versionText].filter(Boolean).join(modelDescription.ownNameVersionSeparator ?? ' ');
  3676. case MODEL_LABEL_TEXT_MODE.VERY_SHORT:
  3677. const abbr = modelDescription.abbrEn;
  3678. if (!abbr) {
  3679. console.warn('[getLabelFromModelDescription] modelDescription.abbrEn is empty', modelDescription);
  3680. } else {
  3681. return abbr;
  3682. }
  3683. const shortName = modelDescription.nameEnShort ?? modelDescription.nameEn;
  3684. return shortName.split(/\s+/).map(word => word.charAt(0)).join('');
  3685. case MODEL_LABEL_TEXT_MODE.FAMILIAR_NAME:
  3686. return modelDescription.familiarNameEn ?? modelDescription.nameEn;
  3687. default:
  3688. throw new Error(`Unknown model label style: ${modelLabelStyle}`);
  3689. }
  3690. };
  3691.  
  3692. const getExtraClassesFromModelLabelStyle = modelLabelStyle => {
  3693. switch (modelLabelStyle) {
  3694. case MODEL_LABEL_STYLE.BUTTON_SUBTLE:
  3695. return modelLabelStyleButtonSubtleCls;
  3696. case MODEL_LABEL_STYLE.BUTTON_WHITE:
  3697. return modelLabelStyleButtonWhiteCls;
  3698. case MODEL_LABEL_STYLE.BUTTON_CYAN:
  3699. return modelLabelStyleButtonCyanCls;
  3700. case MODEL_LABEL_STYLE.NO_TEXT:
  3701. return '';
  3702. default:
  3703. return '';
  3704. }
  3705. };
  3706.  
  3707. const handleModelLabel = () => {
  3708. const config = loadConfigOrDefault();
  3709. if (!config.modelLabelStyle || config.modelLabelStyle === MODEL_LABEL_STYLE.OFF) return;
  3710.  
  3711. const $modelIcons = PP.getAnyModelButton();
  3712. $modelIcons.each((_, el) => {
  3713. const $el = jq(el);
  3714.  
  3715. // Initial setup if elements don't exist yet
  3716. if (!$el.find(`.${modelLabelCls}`).length) {
  3717. $el.prepend(jq(`<span class="${modelLabelCls}"></span>`));
  3718. $el.closest('.col-start-3').removeClass('col-start-3').addClass('col-start-2 col-end-4');
  3719. }
  3720. if (!$el.hasClass(modelIconButtonCls)) {
  3721. $el.addClass(modelIconButtonCls);
  3722. }
  3723.  
  3724. // Get current config state and model information
  3725. const modelDescription = PP.getModelDescriptionFromModelButton($el);
  3726. const modelLabelFromAriaLabel = $el.attr('aria-label');
  3727. const modelLabel = config.modelLabelStyle === MODEL_LABEL_STYLE.NO_TEXT ? '' :
  3728. getLabelFromModelDescription(config.modelLabelTextMode)(modelLabelFromAriaLabel)(modelDescription);
  3729.  
  3730. if (modelLabel === undefined || modelLabel === null) {
  3731. console.error('[handleModelLabel] modelLabel is empty', { modelDescription, modelLabelFromAriaLabel, $el });
  3732. return;
  3733. }
  3734.  
  3735. // Calculate the style classes
  3736. const extraClasses = [
  3737. getExtraClassesFromModelLabelStyle(config.modelLabelStyle),
  3738. config.modelLabelOverwriteCyanIconToGray ? modelLabelOverwriteCyanIconToGrayCls : '',
  3739. ].filter(Boolean).join(' ');
  3740.  
  3741. // Check the current "CPU icon removal" configuration state
  3742. const shouldRemoveCpuIcon = config.modelLabelRemoveCpuIcon;
  3743. const hasCpuIconRemoval = $el.hasClass(modelLabelRemoveCpuIconCls);
  3744.  
  3745. // Only update CPU icon removal class if needed
  3746. if (shouldRemoveCpuIcon !== hasCpuIconRemoval) {
  3747. if (shouldRemoveCpuIcon) {
  3748. $el.addClass(modelLabelRemoveCpuIconCls);
  3749. } else {
  3750. $el.removeClass(modelLabelRemoveCpuIconCls);
  3751. }
  3752. }
  3753.  
  3754. // Handle larger icons setting
  3755. const shouldUseLargerIcons = config.modelLabelLargerIcons;
  3756. const hasLargerIconsClass = $el.hasClass(modelLabelLargerIconsCls);
  3757.  
  3758. // Only update larger icons class if needed
  3759. if (shouldUseLargerIcons !== hasLargerIconsClass) {
  3760. if (shouldUseLargerIcons) {
  3761. $el.addClass(modelLabelLargerIconsCls);
  3762. } else {
  3763. $el.removeClass(modelLabelLargerIconsCls);
  3764. }
  3765. }
  3766.  
  3767. // Work with the label element
  3768. const $label = $el.find(`.${modelLabelCls}`);
  3769.  
  3770. // Use data attributes to track current state
  3771. const storedModelDescriptionStr = $label.attr('data-model-description');
  3772. const storedExtraClasses = $label.attr('data-extra-classes');
  3773. const storedLabel = $label.attr('data-label-text');
  3774.  
  3775. // Only update if something has changed
  3776. const modelDescriptionStr = JSON.stringify(modelDescription);
  3777. const needsUpdate =
  3778. storedModelDescriptionStr !== modelDescriptionStr ||
  3779. storedExtraClasses !== extraClasses ||
  3780. storedLabel !== modelLabel;
  3781.  
  3782. if (needsUpdate) {
  3783. // Store the current state in data attributes
  3784. $label.attr('data-model-description', modelDescriptionStr);
  3785. $label.attr('data-extra-classes', extraClasses);
  3786. $label.attr('data-label-text', modelLabel);
  3787.  
  3788. // Apply the text content
  3789. $label.text(modelLabel);
  3790.  
  3791. // Apply classes only if they've changed
  3792. if (storedExtraClasses !== extraClasses) {
  3793. $label.removeClass(modelLabelStyleButtonSubtleCls)
  3794. .removeClass(modelLabelStyleButtonWhiteCls)
  3795. .removeClass(modelLabelStyleButtonCyanCls)
  3796. .removeClass(modelLabelOverwriteCyanIconToGrayCls)
  3797. .addClass(extraClasses);
  3798. }
  3799. }
  3800.  
  3801. // Handle error icon if errorType exists
  3802. const hasErrorType = modelDescription?.errorType !== undefined;
  3803. const existingErrorIcon = $el.find(`.${errorIconCls}`);
  3804.  
  3805. // Check if we need to add or remove the error icon
  3806. if (hasErrorType && existingErrorIcon.length === 0) {
  3807. // Add the error icon
  3808. const errorIconUrl = getLucideIconUrl('alert-triangle');
  3809. const $errorIcon = jq(`<img src="${errorIconUrl}" alt="Error" class="${errorIconCls}" />`)
  3810. .attr('data-error-type', modelDescription.errorType)
  3811. .css('filter', hexToCssFilter('#FFA500').filter)
  3812. .attr('title', modelDescription.errorString || 'Error: Used fallback model');
  3813.  
  3814. // Insert the error icon at the correct position
  3815. const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`);
  3816. if ($reasoningModelIcon.length > 0) {
  3817. $reasoningModelIcon.after($errorIcon);
  3818. } else {
  3819. $el.prepend($errorIcon);
  3820. }
  3821. } else if (!hasErrorType && existingErrorIcon.length > 0) {
  3822. // Remove the error icon if no longer needed
  3823. existingErrorIcon.remove();
  3824. } else if (hasErrorType && existingErrorIcon.length > 0) {
  3825. // Update the error icon title if it changed
  3826. if (existingErrorIcon.attr('data-error-type') !== modelDescription.errorType) {
  3827. existingErrorIcon
  3828. .attr('data-error-type', modelDescription.errorType)
  3829. .attr('title', modelDescription.errorString || 'Error: Used fallback model');
  3830. }
  3831. }
  3832.  
  3833. // Handle model icon
  3834. if (config.modelLabelIcons && config.modelLabelIcons !== MODEL_LABEL_ICONS.OFF) {
  3835. const existingIcon = $el.find(`.${modelIconCls}`);
  3836.  
  3837. // Get model-specific icon based on model name
  3838. const modelName = modelDescription?.nameEn ?? '';
  3839. const brandIconInfo = getBrandIconInfo(modelName);
  3840. if (!brandIconInfo) {
  3841. // TODO: very spammy, issues with "models" like "Pro Search", "Deep Research" and "Labs"
  3842. debugLog('brandIconInfo is null', { modelName, modelDescription });
  3843. return;
  3844. }
  3845.  
  3846. const { iconName, brandColor } = brandIconInfo;
  3847. const existingIconData = existingIcon.attr('data-model-icon');
  3848. const existingIconMode = existingIcon.attr('data-icon-mode');
  3849.  
  3850. // Check if we need to update the icon
  3851. const shouldUpdateIcon =
  3852. existingIconData !== iconName ||
  3853. existingIcon.length === 0 ||
  3854. existingIconMode !== config.modelLabelIcons;
  3855.  
  3856. if (shouldUpdateIcon) {
  3857. existingIcon.remove();
  3858.  
  3859. if (iconName) {
  3860. const iconUrl = getLobeIconsUrl(iconName);
  3861. const $icon = jq(`<img src="${iconUrl}" alt="Model icon" class="${modelIconCls}" />`)
  3862. .attr('data-model-icon', iconName)
  3863. .attr('data-icon-mode', config.modelLabelIcons);
  3864.  
  3865. // Apply styling based on monochrome/color mode
  3866. if (config.modelLabelIcons === MODEL_LABEL_ICONS.MONOCHROME) {
  3867. // Apply monochrome filter
  3868. $icon.css('filter', 'invert(1)');
  3869.  
  3870. // Apply color classes for monochrome icons based on button style
  3871. if (config.modelLabelStyle === MODEL_LABEL_STYLE.JUST_TEXT) {
  3872. $icon.addClass(iconColorGrayCls);
  3873. } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_CYAN) {
  3874. $icon.addClass(iconColorCyanCls);
  3875. } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_SUBTLE) {
  3876. $icon.addClass(iconColorGrayCls);
  3877. } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_WHITE) {
  3878. $icon.addClass(iconColorWhiteCls);
  3879. }
  3880. } else if (config.modelLabelIcons === MODEL_LABEL_ICONS.COLOR) {
  3881. // Ensure the icon displays in color
  3882. $icon.attr('data-brand-color', brandColor);
  3883. $icon.css('filter', hexToCssFilter(brandColor).filter);
  3884. $icon.attr('data-brand-color-filter', hexToCssFilter(brandColor).filter);
  3885. }
  3886.  
  3887. const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`);
  3888. const $errorIcon = $el.find(`.${errorIconCls}`);
  3889. const hasReasoningModelIcon = $reasoningModelIcon.length !== 0;
  3890. const hasErrorIcon = $errorIcon.length !== 0;
  3891.  
  3892. if (hasReasoningModelIcon) {
  3893. // $icon.css({ marginLeft: '0px' });
  3894. // $el.css({ paddingRight: hasReasoningModelIcon ? '8px' : '2px' });
  3895. $reasoningModelIcon.after($icon);
  3896. } else if (hasErrorIcon) {
  3897. $errorIcon.after($icon);
  3898. } else {
  3899. // $icon.css({ marginLeft: '-2px' });
  3900. $el.prepend($icon);
  3901. }
  3902.  
  3903. // if (!modelLabel) {
  3904. // $icon.css({ marginRight: '-6px', marginLeft: '-2px' });
  3905. // $el.css({ paddingRight: '8px', paddingLeft: '10px' });
  3906. // }
  3907. }
  3908. }
  3909. } else {
  3910. // Remove model icon if setting is off
  3911. $el.find(`.${modelIconCls}`).remove();
  3912. }
  3913.  
  3914. // Handle reasoning model icon
  3915. const isReasoningModel = modelDescription?.modelType === 'reasoning';
  3916. if (config.modelLabelUseIconForReasoningModels !== MODEL_LABEL_ICON_REASONING_MODEL.OFF) {
  3917. const prevReasoningModelIcon = $el.find(`.${reasoningModelCls}`);
  3918. const hasIconSetting = $el.attr('data-reasoning-icon-setting');
  3919. const currentSetting = config.modelLabelUseIconForReasoningModels;
  3920. const currentIconColor = config.modelLabelReasoningModelIconColor || '#ffffff';
  3921. const storedIconColor = $el.attr('data-reasoning-icon-color');
  3922.  
  3923. // Only make changes if the reasoning status, icon setting, or color has changed
  3924. if (hasIconSetting !== currentSetting ||
  3925. (isReasoningModel && prevReasoningModelIcon.length === 0) ||
  3926. (!isReasoningModel && prevReasoningModelIcon.length > 0) ||
  3927. storedIconColor !== currentIconColor) {
  3928.  
  3929. // Update tracking attributes
  3930. $el.attr('data-reasoning-icon-setting', currentSetting);
  3931. $el.attr('data-reasoning-icon-color', currentIconColor);
  3932. $el.attr('data-is-reasoning-model', isReasoningModel);
  3933.  
  3934. // Update reasoning model class as needed
  3935. if (!isReasoningModel) {
  3936. $el.addClass(notReasoningModelCls);
  3937. prevReasoningModelIcon.remove();
  3938. } else {
  3939. $el.removeClass(notReasoningModelCls);
  3940.  
  3941. if (prevReasoningModelIcon.length === 0) {
  3942. const iconUrl = getLucideIconUrl(config.modelLabelUseIconForReasoningModels.toLowerCase().replace(' ', '-'));
  3943. const $icon = jq(`<img src="${iconUrl}" alt="Reasoning model" class="${reasoningModelCls}" />`);
  3944.  
  3945. $icon.css('filter', hexToCssFilter(config.modelLabelReasoningModelIconColor || '#ffffff').filter);
  3946.  
  3947. $el.prepend($icon);
  3948.  
  3949. const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`);
  3950. $reasoningModelIcon.css({ display: 'inline-block' });
  3951.  
  3952. if (config.modelLabelStyle === MODEL_LABEL_STYLE.JUST_TEXT) {
  3953. $reasoningModelIcon.addClass(iconColorGrayCls);
  3954. } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_CYAN) {
  3955. $reasoningModelIcon.addClass(iconColorCyanCls);
  3956. } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_SUBTLE) {
  3957. $reasoningModelIcon.addClass(iconColorGrayCls);
  3958. } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_WHITE) {
  3959. $reasoningModelIcon.addClass(iconColorWhiteCls);
  3960. }
  3961.  
  3962. const $modelLabelIcon = $el.find(`.${modelIconCls}`);
  3963. const $errorIcon = $el.find(`.${errorIconCls}`);
  3964. if ($modelLabelIcon.length !== 0 || $errorIcon.length !== 0) {
  3965. $reasoningModelIcon.css({ marginLeft: '4px' });
  3966. } else {
  3967. $reasoningModelIcon.css({ marginLeft: '0px' });
  3968. }
  3969. } else {
  3970. prevReasoningModelIcon.css('filter', hexToCssFilter(config.modelLabelReasoningModelIconColor || '#ffffff').filter);
  3971. }
  3972. }
  3973. }
  3974. }
  3975. });
  3976. };
  3977.  
  3978. const handleHideDiscoverButton = () => {
  3979. const config = loadConfigOrDefault();
  3980. if (!config.hideDiscoverButton) return;
  3981. const $iconsInLeftPanel = PP.getIconsInLeftPanel().find('a[href^="/discover"]');
  3982. $iconsInLeftPanel.hide();
  3983. };
  3984.  
  3985. const handleCustomModelPopover = () => {
  3986. const config = loadConfigOrDefault();
  3987. const mode = config.customModelPopover;
  3988. if (mode === CUSTOM_MODEL_POPOVER_MODE.OFF) return;
  3989.  
  3990. const $modelSelectionList = PP.getModelSelectionList();
  3991. if ($modelSelectionList.length === 0) return;
  3992. const processedAttr = 'ph-processed-custom-model-popover';
  3993. if ($modelSelectionList.attr(processedAttr)) return;
  3994. $modelSelectionList.attr(processedAttr, true);
  3995.  
  3996. $modelSelectionList.nthParent(2).css({ maxHeight: 'initial' });
  3997.  
  3998. const $reasoningDelim = $modelSelectionList.children(".sm\\:px-sm.relative");
  3999.  
  4000. const markListItemAsReasoningModel = (el) => {
  4001. const $el = jq(el);
  4002. const $icon = jq('<img>', {
  4003. src: getLucideIconUrl(config.modelLabelUseIconForReasoningModels.toLowerCase()),
  4004. alt: 'Reasoning model',
  4005. class: reasoningModelCls,
  4006. }).css({ marginLeft: '0px' });
  4007. $el.find('.cursor-pointer > .flex').first().prepend($icon);
  4008. };
  4009. const modelSelectionListType = PP.getModelSelectionListType($modelSelectionList);
  4010. if (config.modelLabelUseIconForReasoningModels !== MODEL_LABEL_ICON_REASONING_MODEL.OFF) {
  4011. if (modelSelectionListType === 'new') {
  4012. const $delimIndex = $modelSelectionList.children().index($reasoningDelim);
  4013. $modelSelectionList.children().slice($delimIndex + 1).each((_idx, el) => {
  4014. markListItemAsReasoningModel(el);
  4015. });
  4016. } else {
  4017. $modelSelectionList
  4018. .children()
  4019. .filter((_idx, rEl) => jq(rEl).find('span').text().includes('Reasoning'))
  4020. .each((_idx, el) => markListItemAsReasoningModel(el));
  4021. }
  4022. }
  4023. const $delims = $modelSelectionList.children(".sm\\:mx-sm");
  4024.  
  4025. const removeAllDelims = () => {
  4026. $delims.hide();
  4027. $reasoningDelim.hide();
  4028. };
  4029.  
  4030. const removeAllModelDescriptions = () => {
  4031. $modelSelectionList.find('div.light.text-textOff').hide();
  4032. $modelSelectionList.find('.group\\/item > .relative > .gap-sm').css({ alignItems: 'center' });
  4033. };
  4034.  
  4035. if (mode === CUSTOM_MODEL_POPOVER_MODE.COMPACT_LIST) {
  4036. removeAllDelims();
  4037. removeAllModelDescriptions();
  4038. return;
  4039. }
  4040. if (mode === CUSTOM_MODEL_POPOVER_MODE.SIMPLE_LIST) {
  4041. // it is already a list, we forced the height to grow
  4042. return;
  4043. }
  4044.  
  4045. $modelSelectionList.css({
  4046. display: 'grid',
  4047. gridTemplateColumns: '1fr 1fr',
  4048. gap: mode === CUSTOM_MODEL_POPOVER_MODE.COMPACT_GRID ? '0px' : '10px',
  4049. 'grid-auto-rows': 'min-content',
  4050. });
  4051.  
  4052. if (mode === CUSTOM_MODEL_POPOVER_MODE.COMPACT_GRID) {
  4053. removeAllDelims();
  4054. removeAllModelDescriptions();
  4055. }
  4056.  
  4057. $delims.hide();
  4058. $reasoningDelim.css({ gridColumn: 'span 2', });
  4059. };
  4060.  
  4061. const mainCaptionAppliedCls = genCssName('mainCaptionApplied');
  4062. const handleMainCaptionHtml = () => {
  4063. const config = loadConfigOrDefault();
  4064. if (!config.mainCaptionHtmlEnabled) return;
  4065. if (PP.getMainCaption().hasClass(mainCaptionAppliedCls)) return;
  4066. PP.setMainCaptionHtml(config.mainCaptionHtml);
  4067. PP.getMainCaption().addClass(mainCaptionAppliedCls);
  4068. };
  4069.  
  4070. const handleCustomJs = () => {
  4071. const config = loadConfigOrDefault();
  4072. if (!config.customJsEnabled) return;
  4073.  
  4074. try {
  4075. // Use a static key to ensure we only run once per page load
  4076. const dataKey = 'data-' + genCssName('custom-js-applied');
  4077. if (!jq('body').attr(dataKey)) {
  4078. jq('body').attr(dataKey, true);
  4079. // Use Function constructor to evaluate the JS code
  4080. const customJsFn = new Function(config.customJs);
  4081. customJsFn();
  4082. }
  4083. } catch (error) {
  4084. console.error('Error executing custom JS:', error);
  4085. }
  4086. };
  4087.  
  4088. const handleCustomCss = () => {
  4089. const config = loadConfigOrDefault();
  4090. if (!config.customCssEnabled) return;
  4091.  
  4092. try {
  4093. // Check if custom CSS has already been applied
  4094. const dataKey = 'data-' + genCssName('custom-css-applied');
  4095. if (!jq('head').attr(dataKey)) {
  4096. jq('head').attr(dataKey, true);
  4097. const styleElement = jq('<style></style>')
  4098. .addClass(customCssAppliedCls)
  4099. .text(config.customCss);
  4100. jq('head').append(styleElement);
  4101. }
  4102. } catch (error) {
  4103. console.error('Error applying custom CSS:', error);
  4104. }
  4105. };
  4106.  
  4107. const handleCustomWidgetsHtml = () => {
  4108. const config = loadConfigOrDefault();
  4109. if (!config.customWidgetsHtmlEnabled) return;
  4110.  
  4111. try {
  4112. // Check if custom widgets have already been applied
  4113. const dataKey = 'data-' + genCssName('custom-widgets-html-applied');
  4114. if (!jq('body').attr(dataKey)) {
  4115. jq('body').attr(dataKey, true);
  4116. const widgetContainer = jq('<div></div>')
  4117. .addClass(customWidgetsHtmlAppliedCls)
  4118. .html(config.customWidgetsHtml);
  4119. PP.getPromptAreaWrapperOfNewThread().append(widgetContainer);
  4120. }
  4121. } catch (error) {
  4122. console.error('Error applying custom widgets HTML:', error);
  4123. }
  4124. };
  4125.  
  4126. const handleHideSideMenuLabels = () => {
  4127. const config = loadConfigOrDefault();
  4128. if (!config.hideSideMenuLabels) return;
  4129. const $sideMenu = PP.getLeftPanel();
  4130. if ($sideMenu.hasClass(sideMenuLabelsHiddenCls)) return;
  4131. $sideMenu.addClass(sideMenuLabelsHiddenCls);
  4132. };
  4133.  
  4134. const handleRemoveWhiteSpaceOnLeftOfThreadContent = () => {
  4135. const config = loadConfigOrDefault();
  4136. const val = parseFloat(config.leftMarginOfThreadContent);
  4137. if (isNaN(val)) return;
  4138. if (jq('head').find(`#${leftMarginOfThreadContentStylesId}`).length > 0) return;
  4139. jq(`<style id="${leftMarginOfThreadContentStylesId}">.max-w-threadContentWidth { margin-left: ${val}em !important; }</style>`).appendTo("head");
  4140. };
  4141.  
  4142. // Function to apply a tag's actions (works for both regular and toggle tags)
  4143. const applyTagActions = async (tag, options = {}) => {
  4144. const { skipText = false, callbacks = {} } = options;
  4145.  
  4146. debugLog('Applying tag actions for tag:', tag);
  4147.  
  4148. // Apply mode setting
  4149. if (tag.setMode) {
  4150. const mode = tag.setMode.toLowerCase();
  4151. if (mode === 'pro' || mode === 'research' || mode === 'deep-research' || mode === 'dr' || mode === 'lab') {
  4152. // Convert aliases to the actual mode name that PP understands
  4153. const normalizedMode = mode === 'dr' || mode === 'deep-research' ? 'research' : mode;
  4154.  
  4155. try {
  4156. await PP.doSelectQueryMode(normalizedMode);
  4157. debugLog(`[applyTagActions]: Set mode to ${normalizedMode}`);
  4158. wait(50);
  4159. } catch (error) {
  4160. debugLog(`[applyTagActions]: Error setting mode to ${normalizedMode}`, error);
  4161. }
  4162. } else {
  4163. debugLog(`[applyTagActions]: Invalid mode: ${tag.setMode}`);
  4164. }
  4165. }
  4166.  
  4167. // Apply model setting
  4168. if (tag.setModel) {
  4169. try {
  4170. const modelDescriptor = PP.getModelDescriptorFromId(tag.setModel);
  4171. debugLog('[applyTagActions]: set model=', tag.setModel, ' modelDescriptor=', modelDescriptor);
  4172.  
  4173. if (modelDescriptor) {
  4174. await PP.doSelectModel(modelDescriptor.index);
  4175. debugLog(`[applyTagActions]: Selected model ${modelDescriptor.nameEn}`);
  4176. if (callbacks.modelSet) callbacks.modelSet(modelDescriptor);
  4177. } else {
  4178. debugLog(`[applyTagActions]: Model descriptor not found for ${tag.setModel}`);
  4179. }
  4180. } catch (error) {
  4181. debugLog(`[applyTagActions]: Error setting model to ${tag.setModel}`, error);
  4182. }
  4183. }
  4184.  
  4185. // Apply sources setting
  4186. if (tag.setSources) {
  4187. try {
  4188. // Use PP's high-level function that handles the whole process
  4189. await PP.doSetSourcesSelectionListValues()(tag.setSources);
  4190. debugLog(`[applyTagActions]: Sources set to ${tag.setSources}`);
  4191. await PP.sleep(50);
  4192. if (callbacks.sourcesSet) callbacks.sourcesSet();
  4193. } catch (error) {
  4194. logError(`[applyTagActions]: Error setting sources`, error);
  4195. }
  4196. }
  4197.  
  4198. // Add text to prompt if it's not empty and we're not skipping text
  4199. if (!skipText && tag.text && tag.text.trim().length > 0) {
  4200. try {
  4201. const promptArea = PP.getAnyPromptArea();
  4202. if (promptArea.length) {
  4203. const promptAreaRaw = promptArea[0];
  4204. const newText = applyTagToString(tag, promptArea.val(), promptAreaRaw.selectionStart);
  4205. changeValueUsingEvent(promptAreaRaw, newText);
  4206. debugLog(`[applyTagActions]: Applied text: "${tag.text.substring(0, 20)}${tag.text.length > 20 ? '...' : ''}"`);
  4207. if (callbacks.textApplied) callbacks.textApplied(newText);
  4208. } else {
  4209. debugLog(`[applyTagActions]: No prompt area found for text insertion`);
  4210. }
  4211. } catch (error) {
  4212. debugLog(`[applyTagActions]: Error applying text`, error);
  4213. }
  4214. }
  4215. };
  4216.  
  4217. // Function to apply toggled tags' actions when submit is clicked
  4218. const applyToggledTagsOnSubmit = async ($wrapper) => {
  4219. debugLog('Applying toggled tags on submit', { $wrapper });
  4220. const config = loadConfigOrDefault();
  4221.  
  4222. const allTags = parseTagsText(config.tagsText ?? defaultConfig.tagsText);
  4223. const currentContainerType = getPromptWrapperTagContainerType($wrapper);
  4224.  
  4225. if (!currentContainerType) {
  4226. logError('Could not determine current container type, skipping toggled tags application', {
  4227. $wrapper,
  4228. currentContainerType,
  4229. allTags,
  4230. });
  4231. return false;
  4232. }
  4233.  
  4234. // Find all toggled tags that are relevant for the current container type
  4235. const toggledTags = allTags.filter(tag => {
  4236. // First check if it's a toggle tag
  4237. if (!tag.toggleMode) return false;
  4238. const tagId = generateToggleTagId(tag);
  4239. if (!tagId) return false;
  4240. // Check in-memory toggle state first
  4241. const inMemoryToggled = window._phTagToggleState && window._phTagToggleState[tagId] === true;
  4242. // Then fall back to saved state if tagToggleSave is enabled
  4243. const savedToggled = config.tagToggleSave && config.tagToggledStates && config.tagToggledStates[tagId] === true;
  4244. // If neither is toggled, return false
  4245. if (!inMemoryToggled && !savedToggled) return false;
  4246.  
  4247. // Then check if this tag is relevant for the current container
  4248. return isTagRelevantForContainer(currentContainerType)(tag);
  4249. });
  4250.  
  4251. debugLog(`Toggled tags for ${currentContainerType} context:`, toggledTags.length);
  4252.  
  4253. // Apply each toggled tag's actions sequentially, waiting for each to complete
  4254. for (const tag of toggledTags) {
  4255. debugLog(`Applying toggled tag: ${tag.label || 'Unnamed tag'}`);
  4256. try {
  4257. await applyTagActions(tag);
  4258. debugLog(`Successfully applied toggled tag: ${tag.label || 'Unnamed tag'}`);
  4259. } catch (error) {
  4260. logError(`Error applying toggled tag: ${tag.label || 'Unnamed tag'}`, error);
  4261. }
  4262. }
  4263.  
  4264. return toggledTags.length > 0;
  4265. };
  4266.  
  4267. // Function to check if there are active toggled tags
  4268. const hasActiveToggledTags = () => {
  4269. const config = loadConfigOrDefault();
  4270. // Check in-memory toggle states first
  4271. if (window._phTagToggleState && Object.values(window._phTagToggleState).some(state => state === true)) {
  4272. return true;
  4273. }
  4274. // Then check saved toggle states if enabled
  4275. if (!config.tagToggleSave || !config.tagToggledStates) return false;
  4276.  
  4277. // Check if any tags are toggled on in saved state
  4278. return Object.values(config.tagToggledStates).some(state => state === true);
  4279. };
  4280.  
  4281. // Function to check if there are active toggled tags for the current context
  4282. const hasActiveToggledTagsForCurrentContext = ($wrapper) => {
  4283. const config = loadConfigOrDefault();
  4284. const currentContainerType = getPromptWrapperTagContainerType($wrapper);
  4285. // START DEBUG LOGGING
  4286. const wrapperId = $wrapper && $wrapper.length ? ($wrapper.attr('id') || 'wrapper-' + Math.random().toString(36).substring(2, 9)) : 'no-wrapper';
  4287. if (!$wrapper || !$wrapper.length) {
  4288. debugLogTags(`hasActiveToggledTagsForCurrentContext - No valid wrapper provided for ${wrapperId}`);
  4289. return false;
  4290. }
  4291. if (!currentContainerType) {
  4292. debugLogTags(`hasActiveToggledTagsForCurrentContext - No container type for wrapper ${wrapperId}`);
  4293. return false;
  4294. }
  4295. debugLogTags(`hasActiveToggledTagsForCurrentContext - Container type ${currentContainerType} for wrapper ${wrapperId}`);
  4296. // END DEBUG LOGGING
  4297.  
  4298. // Get all tags
  4299. const allTags = parseTagsText(config.tagsText ?? defaultConfig.tagsText);
  4300.  
  4301. // Filter for toggled-on tags relevant to the current context
  4302. const hasActiveTags = allTags.some(tag => {
  4303. if (!tag.toggleMode) return false;
  4304. const tagId = generateToggleTagId(tag);
  4305. if (!tagId) return false;
  4306. // Check in-memory toggle state first
  4307. const inMemoryToggled = window._phTagToggleState && window._phTagToggleState[tagId] === true;
  4308. // Then fall back to saved state if tagToggleSave is enabled
  4309. const savedToggled = config.tagToggleSave && config.tagToggledStates && config.tagToggledStates[tagId] === true;
  4310. // If neither is toggled, return false
  4311. if (!inMemoryToggled && !savedToggled) return false;
  4312.  
  4313. // Check if this tag is relevant for the current container
  4314. const isRelevant = isTagRelevantForContainer(currentContainerType)(tag);
  4315. // DEBUG LOG
  4316. if (inMemoryToggled || savedToggled) {
  4317. debugLogTags(`hasActiveToggledTagsForCurrentContext - Tag ${tag.label || 'unnamed'}: inMemory=${inMemoryToggled}, saved=${savedToggled}, relevant=${isRelevant}`);
  4318. }
  4319. return isRelevant;
  4320. });
  4321. // DEBUG LOG
  4322. debugLogTags(`hasActiveToggledTagsForCurrentContext - Final result for ${wrapperId}: ${hasActiveTags}`);
  4323. return hasActiveTags;
  4324. };
  4325.  
  4326. // Function to get a comma-separated list of active toggled tag labels
  4327. const getActiveToggledTagLabels = ($wrapper) => {
  4328. const config = loadConfigOrDefault();
  4329.  
  4330. // Get all tags
  4331. const allTags = parseTagsText(config.tagsText ?? defaultConfig.tagsText);
  4332.  
  4333. // Filter for toggled-on tags
  4334. const activeTags = allTags.filter(tag => {
  4335. if (!tag.toggleMode) return false;
  4336. const tagId = generateToggleTagId(tag);
  4337. if (!tagId) return false;
  4338. // Check in-memory toggle state first
  4339. const inMemoryToggled = window._phTagToggleState && window._phTagToggleState[tagId] === true;
  4340. // Then fall back to saved state if tagToggleSave is enabled
  4341. const savedToggled = config.tagToggleSave && config.tagToggledStates && config.tagToggledStates[tagId] === true;
  4342. // If neither is toggled, return false
  4343. if (!inMemoryToggled && !savedToggled) return false;
  4344.  
  4345. // If wrapper is provided, check if this tag is relevant for the current container type
  4346. if ($wrapper) {
  4347. const currentContainerType = getPromptWrapperTagContainerType($wrapper);
  4348. if (currentContainerType && !isTagRelevantForContainer(currentContainerType)(tag)) {
  4349. return false;
  4350. }
  4351. }
  4352.  
  4353. return true;
  4354. });
  4355.  
  4356. // Return labels joined by commas
  4357. return activeTags.map(tag => tag.label || 'Unnamed tag').join(', ');
  4358. };
  4359.  
  4360. const mockChromeRuntime = () => {
  4361. if (!window.chrome) {
  4362. window.chrome = {};
  4363. }
  4364. if (!window.chrome.runtime) {
  4365. window.chrome.runtime = {
  4366. _about: 'mock by Perplexity Helper; otherwise clicking on the submit button programmatically crashes in promise',
  4367. sendMessage: function() {
  4368. log('mockChromeRuntime: sendMessage', arguments);
  4369. return Promise.resolve({success: true});
  4370. }
  4371. };
  4372. }
  4373. };
  4374.  
  4375. // Enhanced submit button for toggled tags
  4376. const createEnhancedSubmitButton = (originalButton) => {
  4377. const $originalBtn = jq(originalButton);
  4378. const config = loadConfigOrDefault();
  4379.  
  4380. // Find the proper prompt area wrapper, going up to queryBox class first
  4381. const $queryBox = $originalBtn.closest(`.${queryBoxCls}`);
  4382. const $wrapper = $queryBox.length
  4383. ? $queryBox.parent()
  4384. : $originalBtn.closest('.flex').parent().parent().parent();
  4385.  
  4386. const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper);
  4387. const activeTagLabels = getActiveToggledTagLabels($wrapper);
  4388. const title = activeTagLabels
  4389. ? `Submit with toggled tags applied (${activeTagLabels})`
  4390. : 'Submit with toggled tags applied';
  4391.  
  4392. const $enhancedBtn = jq('<div/>')
  4393. .addClass(enhancedSubmitButtonCls)
  4394. .attr('title', title)
  4395. // ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls
  4396. .toggleClass('active', hasActiveInContext && config.tagToggleModeIndicators)
  4397. .html(`<span class="${enhancedSubmitButtonPhTextCls}">PH</span>`);
  4398.  
  4399. // Add the enhanced button as an overlay on the original
  4400. $originalBtn.css('position', 'relative');
  4401. $originalBtn.append($enhancedBtn);
  4402.  
  4403. // Handle click on enhanced button
  4404. $enhancedBtn.on('click', async (e) => {
  4405. e.preventDefault();
  4406. e.stopPropagation();
  4407.  
  4408. // Show temporary processing indicator
  4409. // ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls
  4410. $enhancedBtn.addClass('active').css('opacity', '1').find(`.${enhancedSubmitButtonPhTextCls}`).text('...');
  4411. // DEBUG
  4412. if (loadConfigOrDefault().debugTagsMode) {
  4413. debugLogTags(`Enhanced button click - adding 'active' class (should be ${enhancedSubmitButtonActiveCls})`);
  4414. }
  4415.  
  4416. const finishProcessing = () => {
  4417. if (loadConfigOrDefault().debugTagsSuppressSubmit) {
  4418. log('Suppressing submit after applying tags');
  4419. return;
  4420. }
  4421. try {
  4422. // $originalBtn[0].click();
  4423. // const event = new MouseEvent('click', {
  4424. // bubbles: true,
  4425. // cancelable: true,
  4426. // });
  4427. // $originalBtn[0].dispatchEvent(event);
  4428. // $originalBtn.trigger('click');
  4429.  
  4430. // Try to make a more authentic-looking click event
  4431. // const clickEvent = new MouseEvent('click', {
  4432. // bubbles: true,
  4433. // cancelable: true,
  4434. // view: window,
  4435. // detail: 1, // number of clicks
  4436. // isTrusted: true // attempt to make it look trusted (though this is readonly)
  4437. // });
  4438. // $originalBtn[0].dispatchEvent(clickEvent);
  4439.  
  4440. // Find the React component's props
  4441. // const reactInstance = Object.keys($originalBtn[0]).find(key => key.startsWith('__reactFiber$'));
  4442. // if (reactInstance) {
  4443. // const props = $originalBtn[0][reactInstance].memoizedProps;
  4444. // if (props && props.onClick) {
  4445. // // Call the handler directly, bypassing the event system
  4446. // props.onClick();
  4447. // } else {
  4448. // logError('[createEnhancedSubmitButton]: No onClick handler found', {
  4449. // $originalBtn,
  4450. // reactInstance,
  4451. // props,
  4452. // });
  4453. // }
  4454. // } else {
  4455. // logError('[createEnhancedSubmitButton]: No React instance found', {
  4456. // $originalBtn,
  4457. // });
  4458. // }
  4459.  
  4460. mockChromeRuntime();
  4461. $originalBtn.trigger('click');
  4462. } catch (error) {
  4463. logError('[createEnhancedSubmitButton]: Error in finishProcessing:', error);
  4464. }
  4465. };
  4466.  
  4467. try {
  4468. // Apply all toggled tags sequentially, waiting for each to complete
  4469. const tagsApplied = await applyToggledTagsOnSubmit($wrapper);
  4470.  
  4471. // Add a small delay after applying all tags to ensure UI updates are complete
  4472. if (tagsApplied) { await PP.sleep(50); }
  4473.  
  4474. // Reset the button appearance
  4475. $enhancedBtn.css('opacity', '').find(`.${enhancedSubmitButtonPhTextCls}`).text('');
  4476. if (!hasActiveInContext) {
  4477. // ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls
  4478. $enhancedBtn.removeClass('active');
  4479. // DEBUG
  4480. if (loadConfigOrDefault().debugTagsMode) {
  4481. debugLogTags(`Enhanced button - removing 'active' class because !hasActiveInContext (should be ${enhancedSubmitButtonActiveCls})`);
  4482. }
  4483. }
  4484.  
  4485. // Trigger the original button click
  4486. finishProcessing();
  4487. } catch (error) {
  4488. console.error('Error in enhanced submit button:', error);
  4489. $enhancedBtn.css('opacity', '').find(`.${enhancedSubmitButtonPhTextCls}`).text('');
  4490. if (!hasActiveInContext) {
  4491. // ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls
  4492. $enhancedBtn.removeClass('active');
  4493. // DEBUG
  4494. if (loadConfigOrDefault().debugTagsMode) {
  4495. debugLogTags(`Enhanced button error handler - removing 'active' class (should be ${enhancedSubmitButtonActiveCls})`);
  4496. }
  4497. }
  4498. // Still attempt to submit even if there was an error
  4499. finishProcessing();
  4500. }
  4501. });
  4502.  
  4503. return $enhancedBtn;
  4504. };
  4505.  
  4506. // Add enhanced submit buttons to handle toggled tags
  4507. const patchSubmitButtonsForToggledTags = () => {
  4508. const config = loadConfigOrDefault();
  4509. // Skip if toggle mode hooks are disabled
  4510. if (!config.toggleModeHooks) return;
  4511. const submitButtons = PP.getSubmitButtonAnyExceptMic();
  4512. if (!submitButtons.length) return;
  4513.  
  4514. submitButtons.each((_, btn) => {
  4515. const $btn = jq(btn);
  4516. if ($btn.attr('data-patched-for-toggled-tags')) return;
  4517.  
  4518. // Create our enhanced button overlay
  4519. createEnhancedSubmitButton(btn);
  4520.  
  4521. // Mark as patched
  4522. $btn.attr('data-patched-for-toggled-tags', 'true');
  4523. });
  4524. };
  4525.  
  4526. // Function to add keypress listeners to prompt areas
  4527. const updateTextareaIndicator = ($textarea) => {
  4528. if (!$textarea || !$textarea.length) return;
  4529. // Get the current config
  4530. const config = loadConfigOrDefault();
  4531. // Get the wrapper
  4532. const $wrapper = PP.getAnyPromptAreaWrapper($textarea.nthParent(4));
  4533. if (!$wrapper || !$wrapper.length) return;
  4534. // Check for active toggled tags in this context
  4535. const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper);
  4536. // Should we show the indicator?
  4537. const shouldShowIndicator = hasActiveInContext && config.tagToggleModeIndicators;
  4538. // Get current state to avoid unnecessary DOM updates
  4539. const currentlyHasClass = $textarea.hasClass(promptAreaKeyListenerCls);
  4540. const currentlyHasIndicator = $textarea.siblings(`.${promptAreaKeyListenerIndicatorCls}`).length > 0;
  4541. // Only update DOM if state has changed
  4542. if (currentlyHasClass !== shouldShowIndicator || currentlyHasIndicator !== shouldShowIndicator) {
  4543. if (shouldShowIndicator) {
  4544. // Apply the class for the glow effect with transition if not already applied
  4545. if (!currentlyHasClass) {
  4546. $textarea.addClass(promptAreaKeyListenerCls);
  4547. }
  4548. // Add the pulse dot indicator if not already present
  4549. if (!currentlyHasIndicator) {
  4550. // Make sure parent has relative positioning for proper indicator positioning
  4551. const $parent = $textarea.parent();
  4552. if ($parent.css('position') !== 'relative') {
  4553. $parent.css('position', 'relative');
  4554. }
  4555. const $indicator = jq('<div>')
  4556. .addClass(promptAreaKeyListenerIndicatorCls)
  4557. .attr('title', 'Toggle tags active - Press Enter to submit');
  4558. $textarea.after($indicator);
  4559. // Force a reflow then add visible class for animation
  4560. $indicator[0].offsetHeight; // Force reflow
  4561. $indicator.addClass('visible');
  4562. }
  4563. } else {
  4564. // Remove the class with transition for fade out
  4565. if (currentlyHasClass) {
  4566. $textarea.removeClass(promptAreaKeyListenerCls);
  4567. }
  4568. // For indicator, first make it invisible with transition, then remove from DOM
  4569. if (currentlyHasIndicator) {
  4570. const $indicator = $textarea.siblings(`.${promptAreaKeyListenerIndicatorCls}`);
  4571. $indicator.removeClass('visible');
  4572. // Remove from DOM after transition completes
  4573. setTimeout(() => {
  4574. if ($indicator.length) $indicator.remove();
  4575. }, 500); // Match the transition duration in CSS
  4576. }
  4577. }
  4578. }
  4579. };
  4580.  
  4581. const addPromptAreaKeyListeners = () => {
  4582. const config = loadConfigOrDefault();
  4583. // Skip if toggle mode hooks are disabled
  4584. if (!config.toggleModeHooks) return;
  4585. // Get all prompt areas
  4586. const promptAreas = PP.getAnyPromptArea();
  4587. if (!promptAreas.length) return;
  4588. // Process textareas that don't have listeners yet
  4589. promptAreas.each((_, textarea) => {
  4590. const $textarea = jq(textarea);
  4591. // Skip if already has a listener
  4592. if ($textarea.attr('data-toggle-keypress-listener')) return;
  4593. // Mark as having a listener to avoid duplicates
  4594. $textarea.attr('data-toggle-keypress-listener', 'true');
  4595. // Add the visual indicator if needed
  4596. updateTextareaIndicator($textarea);
  4597. // Add the keypress listener for Enter key
  4598. $textarea.on('keydown.togglehook', (e) => {
  4599. // Only handle Enter key
  4600. if (e.key === 'Enter' && !e.shiftKey) {
  4601. // Find the wrapper
  4602. const $wrapper = PP.getAnyPromptAreaWrapper($textarea.nthParent(4));
  4603. if (!$wrapper || !$wrapper.length) return;
  4604. // Check if there are active toggled tags for this context
  4605. const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper);
  4606. if (!hasActiveInContext) return;
  4607. // Prevent default behavior
  4608. e.preventDefault();
  4609. e.stopPropagation();
  4610. // Flash the textarea indicator with animation that always plays
  4611. $textarea.removeClass(pulseFocusCls);
  4612. $textarea[0].offsetHeight; // Force reflow to ensure animation plays
  4613. $textarea.addClass(pulseFocusCls);
  4614. setTimeout(() => $textarea.removeClass(pulseFocusCls), 400);
  4615. // Find and click the submit button
  4616. const $submitBtn = PP.submitButtonAny($wrapper);
  4617. // If we found a submit button with an enhanced button overlay, use that
  4618. if ($submitBtn.length && $submitBtn.find(`.${enhancedSubmitButtonCls}`).length) {
  4619. $submitBtn.find(`.${enhancedSubmitButtonCls}`).click();
  4620. } else if ($submitBtn.length) {
  4621. // Otherwise use the regular submit button
  4622. $submitBtn.click();
  4623. }
  4624. return false;
  4625. }
  4626. });
  4627. // Add focus handling to update appearance
  4628. $textarea.on('focus.togglehook', () => {
  4629. // Update indicator on focus
  4630. updateTextareaIndicator($textarea);
  4631. });
  4632. });
  4633. };
  4634.  
  4635. const updateToggleIndicators = () => {
  4636. const config = loadConfigOrDefault();
  4637. // Track state changes with this object for debugging
  4638. const debugStateChanges = {
  4639. totalButtons: 0,
  4640. unchanged: 0,
  4641. titleChanged: 0,
  4642. activeStateChanged: 0,
  4643. stateChanges: []
  4644. };
  4645. // Update all enhanced submit buttons individually
  4646. jq(`.${enhancedSubmitButtonCls}`).each((idx, btn) => {
  4647. const $btn = jq(btn);
  4648. const $originalBtn = $btn.parent();
  4649. const btnId = $btn.attr('id') || `btn-${idx}`;
  4650. debugStateChanges.totalButtons++;
  4651.  
  4652. // Find the proper prompt area wrapper, going up to queryBox class first
  4653. const $queryBox = $originalBtn.closest(`.${queryBoxCls}`);
  4654. const $wrapper = $queryBox.length
  4655. ? $queryBox.parent()
  4656. : $originalBtn.closest('.flex').parent().parent().parent();
  4657. // DEBUGGING - Track button's wrapper
  4658. const wrapperId = $wrapper && $wrapper.length ? ($wrapper.attr('id') || 'wrapper-' + Math.random().toString(36).substring(2, 9)) : 'no-wrapper';
  4659. if (loadConfigOrDefault().debugTagsMode) {
  4660. debugLogTags(`updateToggleIndicators - Button ${btnId} in wrapper ${wrapperId}`);
  4661. }
  4662.  
  4663. const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper);
  4664. const activeTagLabels = getActiveToggledTagLabels($wrapper);
  4665. const title = activeTagLabels
  4666. ? `Submit with toggled tags applied (${activeTagLabels})`
  4667. : 'Submit with toggled tags applied';
  4668.  
  4669. // Get current state to avoid unnecessary DOM updates
  4670. // ISSUE: using hard-coded 'active' class instead of generated enhancedSubmitButtonActiveCls
  4671. const isCurrentlyActive = $btn.hasClass('active');
  4672. const shouldBeActive = hasActiveInContext && config.tagToggleModeIndicators;
  4673. // DEBUG - Log the class mismatch
  4674. if (loadConfigOrDefault().debugTagsMode) {
  4675. const hasGeneratedClass = $btn.hasClass(enhancedSubmitButtonActiveCls);
  4676. if (isCurrentlyActive !== hasGeneratedClass) {
  4677. debugLogTags(`Class mismatch detected for ${btnId}: 'active'=${isCurrentlyActive}, '${enhancedSubmitButtonActiveCls}'=${hasGeneratedClass}`);
  4678. }
  4679. }
  4680. const currentTitle = $btn.attr('title');
  4681. // DEBUGGING - Track state for this button
  4682. const stateChange = {
  4683. btnId,
  4684. wrapperId,
  4685. isCurrentlyActive,
  4686. shouldBeActive,
  4687. stateChanged: isCurrentlyActive !== shouldBeActive,
  4688. titleChanged: currentTitle !== title
  4689. };
  4690. debugStateChanges.stateChanges.push(stateChange);
  4691. // Only update DOM elements if state has actually changed
  4692. if (isCurrentlyActive !== shouldBeActive || currentTitle !== title) {
  4693. // Update title if changed
  4694. if (currentTitle !== title) {
  4695. debugStateChanges.titleChanged++;
  4696. $btn.attr('title', title);
  4697. }
  4698. // Toggle active class with transition effect if state has changed
  4699. if (isCurrentlyActive !== shouldBeActive) {
  4700. debugStateChanges.activeStateChanged++;
  4701. // ISSUE: We're using literal 'active' here instead of enhancedSubmitButtonActiveCls
  4702. // This should be fixed to use the generated class, but we're just logging for now
  4703. // No additional class manipulation needed - CSS transitions handle the animation
  4704. $btn.toggleClass('active', shouldBeActive);
  4705. if (loadConfigOrDefault().debugTagsMode) {
  4706. debugLogTags(`Class toggle for ${btnId}: 'active' changed to ${shouldBeActive}, from ${isCurrentlyActive}`);
  4707. }
  4708. // If transitioning to active, ensure we have proper z-index to show over other elements
  4709. if (shouldBeActive) {
  4710. $originalBtn.css('z-index', '5');
  4711. } else {
  4712. // Reset z-index after transition
  4713. setTimeout(() => $originalBtn.css('z-index', ''), 500);
  4714. }
  4715. }
  4716. // Update outline only if debugging state requires it
  4717. $btn.css({ outline: config.debugTagsSuppressSubmit ? '5px solid red' : 'none' });
  4718. } else {
  4719. debugStateChanges.unchanged++;
  4720. }
  4721. });
  4722. // Log state change stats
  4723. if (loadConfigOrDefault().debugTagsMode) {
  4724. if (debugStateChanges.activeStateChanged > 0) {
  4725. debugLogTags(`updateToggleIndicators - SUMMARY: total=${debugStateChanges.totalButtons}, unchanged=${debugStateChanges.unchanged}, titleChanged=${debugStateChanges.titleChanged}, activeStateChanged=${debugStateChanges.activeStateChanged}`);
  4726. debugLogTags('updateToggleIndicators - State changes:', debugStateChanges.stateChanges.filter(sc => sc.stateChanged));
  4727. }
  4728. }
  4729. // Also update all textarea indicators when toggle mode hooks are enabled
  4730. if (config.toggleModeHooks) {
  4731. // Get all prompt areas with keypress listeners
  4732. const promptAreas = jq('textarea[data-toggle-keypress-listener="true"]');
  4733. if (promptAreas.length) {
  4734. promptAreas.each((_, textarea) => {
  4735. updateTextareaIndicator(jq(textarea));
  4736. });
  4737. }
  4738. }
  4739. };
  4740.  
  4741. // Function to reset all toggle states (both in-memory and saved if tagToggleSave is enabled)
  4742. const resetAllToggleStates = () => {
  4743. // Reset in-memory state
  4744. window._phTagToggleState = {};
  4745. // Reset saved state if tagToggleSave is enabled
  4746. const config = loadConfigOrDefault();
  4747. if (config.tagToggleSave && config.tagToggledStates) {
  4748. const updatedConfig = {
  4749. ...config,
  4750. tagToggledStates: {}
  4751. };
  4752. saveConfig(updatedConfig);
  4753. }
  4754. // Update existing toggle tags directly in the DOM if possible
  4755. const existingToggledTags = jq(`.${tagCls}[data-toggled="true"]`);
  4756. if (existingToggledTags.length > 0) {
  4757. existingToggledTags.each((_, el) => {
  4758. const $el = jq(el);
  4759. const tagData = JSON.parse($el.attr('data-tag') || '{}');
  4760. if (tagData) {
  4761. // Reset visual state back to untoggled
  4762. updateToggleTagState($el, tagData, false);
  4763. }
  4764. });
  4765. } else {
  4766. // If we couldn't find any toggled tags in the DOM (perhaps they were added after),
  4767. // fall back to a full refresh
  4768. refreshTags({ force: true });
  4769. }
  4770. // Update indicators
  4771. updateToggleIndicators();
  4772. };
  4773.  
  4774. // Function to generate a consistent ID for toggle tags
  4775. const generateToggleTagId = tag => {
  4776. if (!tag.toggleMode) return null;
  4777. return `toggle:${(tag.label || '') + ':' + (tag.position || '') + ':' + (tag.color || '')}:${tag.originalIndex || 0}`;
  4778. };
  4779.  
  4780. const work = () => {
  4781. handleModalCreation();
  4782. handleTopSettingsButtonInsertion();
  4783. handleTopSettingsButtonSetup();
  4784. handleSettingsInit();
  4785. handleLeftSettingsButtonSetup();
  4786. handleExtraSpaceBellowLastAnswer();
  4787. handleHideDiscoverButton();
  4788. handleHideSideMenuLabels();
  4789. handleRemoveWhiteSpaceOnLeftOfThreadContent();
  4790. updateToggleIndicators();
  4791. patchSubmitButtonsForToggledTags();
  4792. addPromptAreaKeyListeners();
  4793.  
  4794. const regex = /^https:\/\/www\.perplexity\.ai\/search\/?.*/;
  4795. const currentUrl = jq(location).attr('href');
  4796. const matchedCurrentUrlAsSearchPage = regex.test(currentUrl);
  4797.  
  4798. // debugLog("currentUrl", currentUrl);
  4799. // debugLog("matchedCurrentUrlAsSearchPage", matchedCurrentUrlAsSearchPage);
  4800.  
  4801. if (matchedCurrentUrlAsSearchPage) {
  4802. handleSearchPage();
  4803. }
  4804. };
  4805.  
  4806. const fastWork = () => {
  4807. handleCustomModelPopover();
  4808. handleSlimLeftMenu();
  4809. handleHideHomeWidgets();
  4810. applySideMenuHiding();
  4811. replaceIconsInMenu();
  4812. handleModelLabel();
  4813. handleMainCaptionHtml();
  4814. handleCustomJs();
  4815. handleCustomCss();
  4816. handleCustomWidgetsHtml();
  4817. };
  4818.  
  4819. const fontUrls = {
  4820. Roboto: 'https://fonts.cdnfonts.com/css/roboto',
  4821. Montserrat: 'https://fonts.cdnfonts.com/css/montserrat',
  4822. Lato: 'https://fonts.cdnfonts.com/css/lato',
  4823. Oswald: 'https://fonts.cdnfonts.com/css/oswald-4',
  4824. Raleway: 'https://fonts.cdnfonts.com/css/raleway-5',
  4825. 'Ubuntu Mono': 'https://fonts.cdnfonts.com/css/ubuntu-mono',
  4826. Nunito: 'https://fonts.cdnfonts.com/css/nunito',
  4827. Poppins: 'https://fonts.cdnfonts.com/css/poppins',
  4828. 'Playfair Display': 'https://fonts.cdnfonts.com/css/playfair-display',
  4829. Merriweather: 'https://fonts.cdnfonts.com/css/merriweather',
  4830. 'Fira Sans': 'https://fonts.cdnfonts.com/css/fira-sans',
  4831. Quicksand: 'https://fonts.cdnfonts.com/css/quicksand',
  4832. Comfortaa: 'https://fonts.cdnfonts.com/css/comfortaa-3',
  4833. 'Almendra': 'https://fonts.cdnfonts.com/css/almendra',
  4834. 'Enchanted Land': 'https://fonts.cdnfonts.com/css/enchanted-land',
  4835. 'Cinzel Decorative': 'https://fonts.cdnfonts.com/css/cinzel-decorative',
  4836. 'Orbitron': 'https://fonts.cdnfonts.com/css/orbitron',
  4837. 'Exo 2': 'https://fonts.cdnfonts.com/css/exo-2',
  4838. 'Chakra Petch': 'https://fonts.cdnfonts.com/css/chakra-petch',
  4839. 'Open Sans Condensed': 'https://fonts.cdnfonts.com/css/open-sans-condensed',
  4840. 'Saira Condensed': 'https://fonts.cdnfonts.com/css/saira-condensed',
  4841. Inter: 'https://cdn.jsdelivr.net/npm/@fontsource/inter@4.5.0/index.min.css',
  4842. 'JetBrains Mono': 'https://fonts.cdnfonts.com/css/jetbrains-mono',
  4843. };
  4844.  
  4845. const loadFont = (fontName) => {
  4846. const fontUrl = fontUrls[fontName];
  4847. debugLog('loadFont', { fontName, fontUrl });
  4848. if (fontUrl) {
  4849. const link = document.createElement('link');
  4850. link.rel = 'stylesheet';
  4851. link.href = fontUrl;
  4852. document.head.appendChild(link);
  4853. }
  4854. };
  4855.  
  4856. const setupFixImageGenerationOverlay = () => {
  4857. const config = loadConfigOrDefault();
  4858. if (config.fixImageGenerationOverlay) {
  4859. setInterval(handleFixImageGenerationOverlay, 250);
  4860. }
  4861. };
  4862.  
  4863. (function () {
  4864. if (loadConfigOrDefault()?.debugMode) {
  4865. enableDebugMode();
  4866. }
  4867.  
  4868. debugLog('TAGS_PALETTES', TAGS_PALETTES);
  4869. if (loadConfigOrDefault()?.debugTagsMode) {
  4870. enableTagsDebugging();
  4871. }
  4872. // Initialize in-memory toggle state from saved state if tagToggleSave is enabled
  4873. const config = loadConfigOrDefault();
  4874. if (config.tagToggleSave && config.tagToggledStates) {
  4875. window._phTagToggleState = { ...config.tagToggledStates };
  4876. debugLog('Initialized in-memory toggle state from saved state', window._phTagToggleState);
  4877. } else {
  4878. window._phTagToggleState = {};
  4879. }
  4880.  
  4881. 'use strict';
  4882. jq("head").append(`<style>${styles}</style>`);
  4883.  
  4884. setupTags();
  4885. setupFixImageGenerationOverlay();
  4886.  
  4887. const mainInterval = setInterval(work, 1000);
  4888. // This interval is too fast (100ms) which causes frequent DOM updates
  4889. // and leads to the class toggling issue with 'active' vs enhancedSubmitButtonActiveCls
  4890. const fastInterval = setInterval(fastWork, 100);
  4891. window.ph = {
  4892. stopWork: () => { clearInterval(mainInterval); clearInterval(fastInterval); },
  4893. work,
  4894. fastWork,
  4895. jq,
  4896. showPerplexityHelperSettingsModal,
  4897. enableTagsDebugging: () => { debugTags = true; },
  4898. disableTagsDebugging: () => { debugTags = false; },
  4899. };
  4900.  
  4901. loadFont(loadConfigOrDefault().tagFont);
  4902. loadFont('JetBrains Mono');
  4903.  
  4904. // Auto open settings if enabled
  4905. if (loadConfigOrDefault()?.autoOpenSettings) {
  4906. // Use setTimeout to ensure the DOM is ready
  4907. setTimeout(() => {
  4908. showPerplexityHelperSettingsModal();
  4909. }, 1000);
  4910. }
  4911.  
  4912. console.log(`%c${userscriptName}%c\n %cTiartyos%c & %cmonnef%c\n ... loaded`,
  4913. 'color: #aaffaa; font-size: 1.5rem; background-color: rgba(0, 0, 0, 0.5); padding: 2px;',
  4914. '',
  4915. 'color: #6b02ff; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;',
  4916. '',
  4917. 'color: #aa2cc3; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;',
  4918. '',
  4919. '');
  4920. console.log('to show settings use:\nph.showPerplexityHelperSettingsModal()');
  4921. }());
  4922.  
  4923.