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 5.1
  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/perplex-plus@0.0.18/dist/lib/perplex-plus.js
  17. // @homepageURL https://www.perplexity.ai/
  18. // @license GPL-3.0-or-later
  19. // ==/UserScript==
  20.  
  21. const PP = window.PP.noConflict();
  22. const jq = PP.jq;
  23. const $c = (cls, parent) => jq(`.${cls}`, parent);
  24. const $i = (id, parent) => jq(`#${id}`, parent);
  25. const takeStr = n => str => str.slice(0, n);
  26. const dropStr = n => str => str.slice(n);
  27. const filter = pred => xs => xs.filter(pred);
  28. const nl = '\n';
  29. const markdownConverter = new showdown.Converter({ tables: true });
  30.  
  31. let debugMode = false;
  32. const enableDebugMode = () => {
  33. debugMode = true;
  34. };
  35.  
  36. const userscriptName = 'Perplexity helper';
  37. const logPrefix = `[${userscriptName}]`;
  38.  
  39. const debugLog = (...args) => {
  40. if (debugMode) {
  41. console.debug(logPrefix, ...args);
  42. }
  43. }
  44.  
  45. let debugTags = false;
  46. const debugLogTags = (...args) => {
  47. if (debugTags) {
  48. console.debug(logPrefix, '[tags]', ...args);
  49. }
  50. }
  51.  
  52. const enableTagsDebugging = () => {
  53. debugTags = true;
  54. }
  55.  
  56. ($ => {
  57. $.fn.nthParent = function (n) {
  58. let $p = $(this);
  59. if (!(n > -0)) { return $() }
  60. let p = 1 + n;
  61. while (p--) { $p = $p.parent(); }
  62. return $p;
  63. };
  64. })(jq);
  65.  
  66. // unpkg had quite often problems, tens of seconds to load, sometime 503 fails
  67. // const getLucideIconUrl = iconName => `https://unpkg.com/lucide-static@latest/icons/${iconName}.svg`;
  68. const getLucideIconUrl = (iconName) => `https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/${iconName}.svg`;
  69.  
  70. const getTDesignIconUrl = iconName => `https://api.iconify.design/tdesign:${iconName}.svg`;
  71.  
  72. const parseIconName = iconName => {
  73. if (!iconName.includes(':')) return { typePrefix: 'l', processedIconName: iconName };
  74. const [typePrefix, processedIconName] = iconName.split(':');
  75. return { typePrefix, processedIconName };
  76. };
  77.  
  78. const getIconUrl = iconName => {
  79. const {typePrefix, processedIconName} = parseIconName(iconName);
  80. if (typePrefix === 'td') {
  81. return getTDesignIconUrl(processedIconName);
  82. }
  83. if (typePrefix === 'l') {
  84. return getLucideIconUrl(processedIconName);
  85. }
  86. throw new Error(`Unknown icon type: ${typePrefix}`);
  87. }
  88.  
  89. const pplxHelperTag = 'pplx-helper';
  90. const genCssName = x => `${pplxHelperTag}--${x}`;
  91.  
  92. 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}" >
  93. <div class="flex items-center leading-none justify-center gap-xs">
  94. ${icoName}
  95. </div></button>`;
  96.  
  97. const upperButton = (id, icoName, title) => `
  98. <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>
  99. `
  100.  
  101. const textButton = (id, text, title) => `
  102. <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">
  103. <div class="flex items-center leading-none justify-center gap-xs"><span class="flex items-center relative ">${text}</span></div></button>
  104. `
  105. const icoColor = '#1F1F1F';
  106. 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>`;
  107. 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;"/>
  108. <g transform="matrix(14.135,0,0,14.135,329.029,-28.2701)">
  109. <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;"/>
  110. </g></svg>`;
  111.  
  112. 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"
  113. \t xml:space="preserve">
  114. <g>
  115. \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
  116. \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
  117. \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
  118. \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
  119. \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
  120. \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
  121. \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
  122. \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
  123. \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
  124. \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
  125. \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
  126. \t\tC32.443,9,30.766,10.677,28.701,10.677z"/>
  127. </g>
  128. </svg>`;
  129.  
  130.  
  131. const perplexityHelperModalId = 'perplexityHelperModal';
  132. const getPerplexityHelperModal = () => $i(perplexityHelperModalId);
  133.  
  134. const modalSettingsTitleCls = genCssName('modal-settings-title');
  135.  
  136. const gitlabLogo = classes => `
  137. <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>
  138. `;
  139.  
  140. const modalLargeIconAnchorClasses = 'hover:scale-110 opacity-50 hover:opacity-100 transition-all duration-300';
  141.  
  142. const modalTabGroupTabsCls = genCssName('modal-tab-group-tabs');
  143. const modalTabGroupActiveCls = genCssName('modal-tab-group-active');
  144. const modalTabGroupContentCls = genCssName('modal-tab-group-content');
  145. const modalTabGroupSeparatorCls = genCssName('modal-tab-group-separator');
  146.  
  147. const modalHTML = `
  148. <div id="${perplexityHelperModalId}" class="modal">
  149. <div class="modal-content">
  150. <span class="close">&times;</span>
  151. <h1 class="flex items-center gap-4">
  152. <span class="mr-4 ${modalSettingsTitleCls}">Perplexity Helper</span>
  153. <a href="https://gitlab.com/Tiartyos/perplexity-helper"
  154. target="_blank" title="GitLab Repository"
  155. class="${modalLargeIconAnchorClasses}"
  156. >
  157. ${gitlabLogo('w-8 h-8 invert')}
  158. </a>
  159. <a href="https://tiartyos.gitlab.io/perplexity-helper/"
  160. target="_blank" title="Web Page"
  161. class="${modalLargeIconAnchorClasses}"
  162. >
  163. <img src="${getLucideIconUrl('globe')}" class="w-8 h-8 invert">
  164. </a>
  165. </h1>
  166. <p class="text-xs opacity-30 mt-1 mb-3">Changes may require page refresh.</p>
  167. <div class="${modalTabGroupTabsCls}">
  168. </div>
  169. <hr class="!mt-0 !mb-0 ${modalTabGroupSeparatorCls}">
  170. </div>
  171. </div>
  172. `;
  173.  
  174. const tagsContainerCls = genCssName('tags-container');
  175. const tagContainerCompactCls = genCssName('tag-container-compact');
  176. const tagContainerWiderCls = genCssName('tag-container-wider');
  177. const tagContainerWideCls = genCssName('tag-container-wide');
  178. const tagContainerExtraWideCls = genCssName('tag-container-extra-wide');
  179. const threadTagContainerCls = genCssName('thread-tag-container');
  180. const newTagContainerCls = genCssName('new-tag-container');
  181. const newTagContainerInCollectionCls = genCssName('new-tag-container-in-collection');
  182. const tagCls = genCssName('tag');
  183. const tagDarkTextCls = genCssName('tag-dark-text');
  184. const tagIconCls = genCssName('tag-icon');
  185. const tagPaletteCls = genCssName('tag-palette');
  186. const tagPaletteItemCls = genCssName('tag-palette-item');
  187. const tagTweakNoBorderCls = genCssName('tag-tweak-no-border');
  188. const tagTweakSlimPaddingCls = genCssName('tag-tweak-slim-padding');
  189. const tagsPreviewCls = genCssName('tags-preview');
  190. const tagsPreviewNewCls = genCssName('tags-preview-new');
  191. const tagsPreviewThreadCls = genCssName('tags-preview-thread');
  192. const tagsPreviewNewInCollectionCls = genCssName('tags-preview-new-in-collection');
  193. const tagTweakTextShadowCls = genCssName('tag-tweak-text-shadow');
  194. const tagFenceCls = genCssName('tag-fence');
  195. const tagAllFencesWrapperCls = genCssName('tag-all-fences-wrapper');
  196. const tagRestOfTagsWrapperCls = genCssName('tag-rest-of-tags-wrapper');
  197. const tagFenceContentCls = genCssName('tag-fence-content');
  198. const tagDirectoryCls = genCssName('tag-directory');
  199. const tagDirectoryContentCls = genCssName('tag-directory-content');
  200. const helpTextCls = genCssName('help-text');
  201. const queryBoxCls = genCssName('query-box');
  202. const controlsAreaCls = genCssName('controls-area');
  203. const textAreaCls = genCssName('text-area');
  204. const standardButtonCls = genCssName('standard-button');
  205. const lucideIconParentCls = genCssName('lucide-icon-parent');
  206. const roundedMD = genCssName('rounded-md');
  207. const leftPanelSlimCls = genCssName('left-panel-slim');
  208. const modelIconButtonCls = genCssName('model-icon-button');
  209. const modelLabelCls = genCssName('model-label');
  210. const modelLabelStyleJustTextCls = genCssName('model-label-style-just-text');
  211. const modelLabelStyleButtonSubtleCls = genCssName('model-label-style-button-subtle');
  212. const modelLabelStyleButtonWhiteCls = genCssName('model-label-style-button-white');
  213. const modelLabelStyleButtonCyanCls = genCssName('model-label-style-button-cyan');
  214. const modelLabelOverwriteCyanIconToGrayCls = genCssName('model-label-overwrite-cyan-icon-to-gray');
  215. const reasoningModelCls = genCssName('reasoning-model');
  216. const iconColorCyanCls = genCssName('icon-color-cyan');
  217. const iconColorGrayCls = genCssName('icon-color-gray');
  218. const iconColorWhiteCls = genCssName('icon-color-white');
  219. const iconColorGoldCls = genCssName('icon-color-gold');
  220.  
  221. const topSettingsButtonId = genCssName('settings-button-top');
  222. const leftSettingsButtonId = genCssName('settings-button-left');
  223. const leftSettingsButtonWrapperId = genCssName('settings-button-left-wrapper');
  224.  
  225. const cyanPerplexityColor = '#1fb8cd';
  226. const cyanMediumPerplexityColor = '#204b51';
  227. const cyanDarkPerplexityColor = '#203133';
  228.  
  229. const styles = `
  230. .textarea_wrapper {
  231. display: flex;
  232. flex-direction: column;
  233. }
  234.  
  235. @import url('https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600&display=swap');
  236.  
  237. .textarea_wrapper > textarea {
  238. width: 100%;
  239. background-color: rgba(0, 0, 0, 0.8);
  240. padding: 0 5px;
  241. border-radius: 0.5em;
  242. }
  243.  
  244. .textarea_label {
  245. }
  246.  
  247. .${helpTextCls} {
  248. background-color: #225;
  249. padding: 0.3em 0.7em;
  250. border-radius: 0.5em;
  251. margin: 1em 0;
  252. }
  253. .${helpTextCls} {
  254. cursor: text;
  255. }
  256.  
  257. .${helpTextCls} a {
  258. text-decoration: underline;
  259. }
  260. .${helpTextCls} a:hover {
  261. color: white;
  262. }
  263.  
  264. .${helpTextCls} code {
  265. font-size: 80%;
  266. background-color: rgba(255, 255, 255, 0.1);
  267. border-radius: 0.3em;
  268. padding: 0.1em;
  269. }
  270. .${helpTextCls} pre > code {
  271. background: none;
  272. }
  273. .${helpTextCls} pre {
  274. font-size: 80%;
  275. overflow: auto;
  276. background-color: rgba(255, 255, 255, 0.1);
  277. border-radius: 0.3em;
  278. padding: 0.1em 1em;
  279. }
  280. .${helpTextCls} li {
  281. list-style: circle;
  282. margin-left: 1em;
  283. }
  284. .${helpTextCls} hr {
  285. margin: 1em 0 0.5em 0;
  286. border-color: rgba(255, 255, 255, 0.1);
  287. }
  288.  
  289. .${helpTextCls} table {
  290. border: 1px solid rgba(255, 255, 255, 0.1);
  291. border-radius: 0.5em;
  292. display: inline-block;
  293. }
  294. .${helpTextCls} table td, .${helpTextCls} table th {
  295. padding: 0.1em 0.5em;
  296. }
  297.  
  298. .btn-helper {
  299. margin-left: 20px
  300. }
  301.  
  302. .modal {
  303. display: none;
  304. position: fixed;
  305. z-index: 1000;
  306. left: 0;
  307. top: 0;
  308. width: 100%;
  309. height: 100%;
  310. overflow: auto;
  311. background-color: rgba(0, 0, 0, 0.8)
  312. }
  313.  
  314. .modal-content {
  315. display: flex;
  316. margin: 1em auto;
  317. width: calc(100vw - 2em);
  318. padding: 20px;
  319. border: 1px solid #333;
  320. background-color: #202025;
  321. border-radius: 6px;
  322. color: rgb(206, 206, 210);
  323. flex-direction: column;
  324. position: relative;
  325. overflow-y: auto;
  326. cursor: default;
  327. font-family: 'Fira Sans', sans-serif;
  328. }
  329.  
  330. .${modalTabGroupTabsCls} {
  331. display: flex;
  332. flex-direction: row;
  333. }
  334.  
  335. .modal-content .${modalTabGroupTabsCls} > button {
  336. border-radius: 0.5em 0.5em 0 0;
  337. border-bottom: 0;
  338. padding: 0.2em 0.5em 0 0.5em;
  339. background-color: #1e293b;
  340. color: rgba(255, 255, 255, 0.5);
  341. outline-bottom: none;
  342. }
  343.  
  344. .modal-content .${modalTabGroupTabsCls} > button.${modalTabGroupActiveCls} {
  345. /* background-color: #3b82f6; */
  346. color: white;
  347. text-shadow: 0 0 1px currentColor;
  348. padding: 0.3em 0.5em 0.2em 0.5em;
  349. }
  350.  
  351. .modal-content .${modalTabGroupContentCls} {
  352. display: flex;
  353. flex-direction: column;
  354. gap: 1em;
  355. padding-top: 1em;
  356. }
  357.  
  358. .${modalSettingsTitleCls} {
  359. background: linear-gradient(to bottom, white, gray);
  360. -webkit-background-clip: text;
  361. background-clip: text;
  362. -webkit-text-fill-color: transparent;
  363. font-weight: bold;
  364. font-size: 3em;
  365. text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
  366. user-select: none;
  367. margin-top: -0.33em;
  368. margin-bottom: -0.33em;
  369. }
  370.  
  371. .modal-content .hover\\:scale-110:hover {
  372. transform: scale(1.1);
  373. }
  374.  
  375. .modal-content label {
  376. padding-right: 10px;
  377. }
  378.  
  379. .modal-content hr {
  380. height: 1px;
  381. margin: 1em 0;
  382. border-color: rgba(255, 255, 255, 0.1);
  383. }
  384.  
  385. .modal-content hr.${modalTabGroupSeparatorCls} {
  386. margin: 0 -1em 0 -1em;
  387. }
  388.  
  389. .modal-content input[type="checkbox"] {
  390. appearance: none;
  391. width: 1.2em;
  392. height: 1.2em;
  393. border: 2px solid #ffffff80;
  394. border-radius: 0.25em;
  395. background-color: transparent;
  396. transition: all 0.2s ease;
  397. cursor: pointer;
  398. position: relative;
  399. }
  400.  
  401. .modal-content input[type="checkbox"]:checked {
  402. background-color: #3b82f6;
  403. border-color: #3b82f6;
  404. }
  405.  
  406. .modal-content input[type="checkbox"]:checked::after {
  407. content: '';
  408. position: absolute;
  409. left: 50%;
  410. top: 50%;
  411. width: 0.4em;
  412. height: 0.7em;
  413. border: solid white;
  414. border-width: 0 2px 2px 0;
  415. transform: translate(-50%, -60%) rotate(45deg);
  416. }
  417.  
  418. .modal-content input[type="checkbox"]:hover {
  419. border-color: #ffffff;
  420. }
  421.  
  422. .modal-content input[type="checkbox"]:focus {
  423. outline: 2px solid #3b82f680;
  424. outline-offset: 2px;
  425. }
  426.  
  427. .modal-content .checkbox_label {
  428. color: white;
  429. line-height: 1.5;
  430. }
  431.  
  432. .modal-content .checkbox_wrapper {
  433. display: flex;
  434. align-items: center;
  435. gap: 0.5em;
  436. }
  437.  
  438. .modal-content .number_label {
  439. margin-left: 0.5em;
  440. }
  441.  
  442. .modal-content .color_wrapper {
  443. display: flex;
  444. align-items: center;
  445. }
  446.  
  447. .modal-content .color_label {
  448. margin-left: 0.5em;
  449. }
  450.  
  451. .modal-content input, .modal-content button {
  452. background-color: #1e293b;
  453. border: 2px solid #ffffff80;
  454. border-radius: 0.5em;
  455. color: white;
  456. padding: 0.5em;
  457. transition: border-color 0.3s ease, outline 0.3s ease;
  458. }
  459.  
  460. .modal-content input:hover, .modal-content button:hover {
  461. border-color: #ffffff;
  462. }
  463.  
  464. .modal-content input:focus, .modal-content button:focus {
  465. outline: 2px solid #3b82f680;
  466. outline-offset: 2px;
  467. }
  468.  
  469. .modal-content input[type="number"] {
  470. padding: 0.5em;
  471. transition: border-color 0.3s ease, outline 0.3s ease;
  472. }
  473.  
  474. .modal-content input[type="color"] {
  475. padding: 0;
  476. height: 2em;
  477. }
  478.  
  479. .modal-content input[type="color"]:hover {
  480. border-color: #ffffff;
  481. }
  482.  
  483. .modal-content input[type="color"]:focus {
  484. outline: 2px solid #3b82f680;
  485. outline-offset: 2px;
  486. }
  487.  
  488. .modal-content h1 + hr {
  489. margin-top: 0.5em;
  490. }
  491.  
  492.  
  493. .modal-content select {
  494. appearance: none;
  495. background-color: #1e293b; /* Dark blue background */
  496. border: 2px solid #ffffff80;
  497. border-radius: 0.5em;
  498. padding: 0.3em 2em 0.3em 0.5em;
  499. color: white;
  500. font-size: 1em;
  501. cursor: pointer;
  502. transition: all 0.2s ease;
  503. 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");
  504. background-repeat: no-repeat;
  505. background-position: right 0.5em center;
  506. background-size: 1.2em;
  507. }
  508.  
  509. .modal-content select option {
  510. background-color: #1e293b; /* Match select background */
  511. color: white;
  512. padding: 0.5em;
  513. }
  514.  
  515. .modal-content select:hover {
  516. border-color: #ffffff;
  517. }
  518.  
  519. .modal-content select:focus {
  520. outline: 2px solid #3b82f680;
  521. outline-offset: 2px;
  522. }
  523.  
  524. .modal-content .select_label {
  525. color: white;
  526. margin-left: 0.5em;
  527. }
  528.  
  529. .modal-content .select_wrapper {
  530. display: flex;
  531. align-items: center;
  532. }
  533.  
  534. .close {
  535. color: rgb(206, 206, 210);
  536. float: right;
  537. font-size: 28px;
  538. font-weight: bold;
  539. position: absolute;
  540. right: 20px;
  541. top: 5px;
  542. }
  543.  
  544. .close:hover,
  545. .close:focus {
  546. color: white;
  547. text-decoration: none;
  548. cursor: pointer;
  549. }
  550.  
  551. #copied-modal,#copied-modal-2 {
  552. padding: 5px 5px;
  553. background:gray;
  554. position:absolute;
  555. display: none;
  556. color: white;
  557. font-size: 15px;
  558. }
  559.  
  560. label > div.select-none {
  561. user-select: text;
  562. cursor: initial;
  563. }
  564.  
  565. .${tagsContainerCls} {
  566. display: flex;
  567. flex-direction: row;
  568. margin: 5px 0;
  569. }
  570. .${tagsContainerCls}.${threadTagContainerCls} {
  571. margin-left: 0.5em;
  572. margin-right: 0.5em;
  573. margin-bottom: 2px;
  574. }
  575.  
  576. .${tagContainerCompactCls} {
  577. margin-top: -2em;
  578. margin-bottom: 1px;
  579. }
  580. .${tagContainerCompactCls} .${tagFenceCls} {
  581. margin: 0;
  582. padding: 1px;
  583. }
  584. .${tagContainerCompactCls} .${tagCls} {
  585. }
  586. .${tagContainerCompactCls} .${tagAllFencesWrapperCls} {
  587. gap: 1px;
  588. }
  589. .${tagContainerCompactCls} .${tagRestOfTagsWrapperCls} {
  590. margin: 1px;
  591. }
  592. .${tagContainerCompactCls} .${tagRestOfTagsWrapperCls},
  593. .${tagContainerCompactCls} .${tagFenceContentCls},
  594. .${tagContainerCompactCls} .${tagDirectoryContentCls} {
  595. gap: 1px;
  596. }
  597.  
  598. .${tagContainerWiderCls} {
  599. margin-left: -6em;
  600. margin-right: -6em;
  601. margin-bottom: 2em;
  602. }
  603. .${tagContainerWiderCls} .${tagCls} {
  604. }
  605.  
  606. .${tagContainerWideCls} {
  607. margin-left: -12em;
  608. margin-right: -12em;
  609. margin-bottom: 3em;
  610. }
  611.  
  612. .${tagContainerExtraWideCls} {
  613. margin-left: -16em;
  614. margin-right: -16em;
  615. margin-bottom: 3em;
  616. }
  617.  
  618. .${tagCls} {
  619. border: 1px solid #3b3b3b;
  620. background-color: #282828;
  621. /*color: rgba(255, 255, 255, 0.482);*/ /* equivalent of #909090; when on #282828 background */
  622. padding: 0px 8px 0 8px;
  623. border-radius: 4px;
  624. cursor: pointer;
  625. transition: background-color 0.2s, color 0.2s;
  626. display: inline-block;
  627. color: #E8E8E6;
  628. user-select: none;
  629. }
  630. .${tagCls}.${tagDarkTextCls} {
  631. color: #171719;
  632. }
  633. .${tagCls} span {
  634. display: inline-block;
  635. }
  636.  
  637. .${tagCls}.${tagTweakNoBorderCls} {
  638. border: none;
  639. }
  640.  
  641. .${tagCls}.${tagTweakSlimPaddingCls} {
  642. padding: 0px 4px 0 4px;
  643. }
  644.  
  645. .${tagCls} .${tagIconCls} {
  646. width: 16px;
  647. height: 16px;
  648. margin-right: 2px;
  649. margin-left: -4px;
  650. margin-top: -4px;
  651. vertical-align: middle;
  652. display: inline-block;
  653. filter: invert(1);
  654. }
  655. .${tagCls}.${tagDarkTextCls} .${tagIconCls} {
  656. filter: none;
  657. }
  658. .${tagCls}.${tagTweakSlimPaddingCls} .${tagIconCls} {
  659. margin-left: -2px;
  660. }
  661. .${tagCls} span {
  662. position: relative;
  663. top: 1.5px;
  664. }
  665. .${tagCls}.${tagTweakTextShadowCls} span {
  666. text-shadow: 1px 0 0.5px black, -1px 0 0.5px black, 0 1px 0.5px black, 0 -1px 0.5px black;
  667. }
  668. .${tagCls}.${tagTweakTextShadowCls}.${tagDarkTextCls} span {
  669. text-shadow: 1px 0 0.5px white, -1px 0 0.5px white, 0 1px 0.5px white, 0 -1px 0.5px white;
  670. }
  671. .${tagCls}:hover {
  672. background-color: #333;
  673. color: #fff;
  674. transform: scale(1.02);
  675. }
  676. .${tagCls}.${tagDarkTextCls}:hover {
  677. /* color: #171717; */
  678. color: #2f2f2f;
  679. }
  680. .${tagCls}:active {
  681. transform: scale(0.98);
  682. }
  683.  
  684. .${tagPaletteCls} {
  685. display: flex;
  686. flex-wrap: wrap;
  687. gap: 1px;
  688. }
  689. .${tagPaletteCls} .${tagPaletteItemCls} {
  690. text-shadow: 1px 0 1px black, -1px 0 1px black, 0 1px 1px black, 0 -1px 1px black;
  691. width: 40px;
  692. height: 25px;
  693. display: inline-block;
  694. text-align: center;
  695. padding: 0 2px;
  696. transition: color 0.2s, border 0.1s;
  697. border: 2px solid transparent;
  698. }
  699.  
  700. .${tagPaletteItemCls}:hover {
  701. cursor: pointer;
  702. color: white;
  703. border: 2px solid white;
  704. }
  705.  
  706. .${tagsPreviewCls} {
  707. background-color: #191a1a;
  708. padding: 0.5em 1em;
  709. border-radius: 1em;
  710. }
  711.  
  712. .${tagAllFencesWrapperCls} {
  713. display: flex;
  714. flex-direction: row;
  715. gap: 5px;
  716. }
  717.  
  718. .${tagRestOfTagsWrapperCls} {
  719. display: flex;
  720. flex-direction: row;
  721. flex-wrap: wrap;
  722. align-content: flex-start;
  723. gap: 5px;
  724. margin: 8px;
  725. }
  726.  
  727. .${tagFenceCls} {
  728. display: flex;
  729. margin: 5px 0;
  730. padding: 5px;
  731. border-radius: 4px;
  732. }
  733.  
  734. .${tagFenceContentCls} {
  735. display: flex;
  736. flex-direction: column;
  737. flex-wrap: wrap;
  738. gap: 5px;
  739. }
  740.  
  741. .${tagDirectoryCls} {
  742. position: relative;
  743. display: flex;
  744. z-index: 100;
  745. }
  746. .${tagDirectoryCls}:hover .${tagDirectoryContentCls} {
  747. display: flex;
  748. }
  749. .${tagDirectoryContentCls} {
  750. position: absolute;
  751. display: none;
  752. flex-direction: column;
  753. gap: 5px;
  754. top: 0px;
  755. padding-bottom: 1px;
  756. left: -5px;
  757. transform: translateY(-100%);
  758. background: rgba(0, 0, 0, 0.5);
  759. padding: 5px;
  760. border-radius: 4px;
  761. flex-wrap: nowrap;
  762. width: max-content;
  763. }
  764. .${tagDirectoryContentCls} .${tagCls} {
  765. white-space: nowrap;
  766. width: fit-content;
  767. }
  768.  
  769. .${queryBoxCls} {
  770. flex-wrap: wrap;
  771. }
  772.  
  773. .${controlsAreaCls} {
  774. grid-template-columns: repeat(4,minmax(0,1fr))
  775. }
  776.  
  777. .${textAreaCls} {
  778. grid-column-end: 5;
  779. }
  780.  
  781. .${standardButtonCls} {
  782. grid-column-start: 4;
  783. }
  784.  
  785. .${roundedMD} {
  786. border-radius: 0.375rem!important;
  787. }
  788.  
  789. #${leftSettingsButtonId} svg {
  790. transition: fill 0.2s;
  791. }
  792. #${leftSettingsButtonId}:hover svg {
  793. fill: #fff !important;
  794. }
  795.  
  796. .w-collapsedSideBarWidth #${leftSettingsButtonId} span {
  797. display: none;
  798. }
  799.  
  800. .w-collapsedSideBarWidth #${leftSettingsButtonId} {
  801. width: 100%;
  802. border-radius: 0.25rem;
  803. height: 40px;
  804. }
  805.  
  806. #${leftSettingsButtonWrapperId} {
  807. display: flex;
  808. padding: 0.1em 0.2em;
  809. justify-content: flex-start;
  810. }
  811.  
  812. .w-collapsedSideBarWidth #${leftSettingsButtonWrapperId} {
  813. justify-content: center;
  814. }
  815.  
  816. .${lucideIconParentCls} > img {
  817. transition: opacity 0.2s ease;
  818. }
  819.  
  820. .${lucideIconParentCls}:hover > img, a.dark\\:text-textMainDark .${lucideIconParentCls} > img {
  821. opacity: 1;
  822. }
  823.  
  824. .${leftPanelSlimCls}.w-collapsedSideBarWidth,
  825. .${leftPanelSlimCls} .w-collapsedSideBarWidth {
  826. width: 50px;
  827. }
  828.  
  829. /* active marker */
  830. .${leftPanelSlimCls} .w-collapsedSideBarWidth .absolute.rounded-l-sm.right-0 {
  831. /* transform: translateX(-3px) !important; */
  832. right: 3px;
  833. }
  834.  
  835. .${modelLabelCls} {
  836. color: #888;
  837. /* padding is from style attr */
  838. transition: color 0.2s, background-color 0.2s, border 0.2s;
  839. }
  840. button:hover > .${modelLabelCls} {
  841. color: #fff;
  842. }
  843. button:has(> .${modelLabelCls}) {
  844. padding-right: 0.75em;
  845. }
  846.  
  847. button:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}) {
  848. border: 1px solid #333;
  849. }
  850. button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}) {
  851. background: #333 !important;
  852. }
  853.  
  854. .${modelLabelCls}.${modelLabelStyleButtonWhiteCls} {
  855. color: #8D9191 !important;
  856. }
  857. button:hover > .${modelLabelCls}.${modelLabelStyleButtonWhiteCls} {
  858. color: #fff !important;
  859. }
  860. .${modelIconButtonCls} svg[stroke] {
  861. stroke: #8D9191 !important;
  862. }
  863. .${modelIconButtonCls}:hover svg[stroke] {
  864. stroke: #fff !important;
  865. }
  866. button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}) {
  867. background: #191A1A !important;
  868. color: #2D2F2F !important;
  869. }
  870. button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}):hover {
  871. color: #8D9191 !important;
  872. }
  873.  
  874. .${modelLabelCls}.${modelLabelStyleButtonCyanCls} {
  875. color: ${cyanPerplexityColor};
  876. }
  877. button:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) {
  878. border: 1px solid ${cyanMediumPerplexityColor};
  879. background: ${cyanDarkPerplexityColor} !important;
  880. }
  881. button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) {
  882. border: 1px solid ${cyanPerplexityColor};
  883. }
  884. .${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) svg[stroke] {
  885. stroke: ${cyanPerplexityColor} !important;
  886. }
  887. .${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}):hover svg[stroke] {
  888. stroke: #fff !important;
  889. }
  890.  
  891. button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}) {
  892. color: #888 !important;
  893. }
  894. button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}):hover {
  895. color: #fff !important;
  896. }
  897.  
  898. .${reasoningModelCls} {
  899. width: 16px;
  900. height: 16px;
  901. margin-right: 4px;
  902. margin-left: 8px;
  903. margin-top: -2px;
  904. filter: invert();
  905. }
  906. button:has(.${reasoningModelCls}) > div > div > svg {
  907. width: 32px;
  908. height: 16px;
  909. margin-left: 8px;
  910. margin-right: 12px;
  911. margin-top: 0px;
  912. min-width: 16px;
  913. }
  914. button:has(.${reasoningModelCls}) > div > div:has(svg) {
  915. width: 16px;
  916. height: 16px;
  917. min-width: 30px;
  918. }
  919.  
  920.  
  921. .${iconColorCyanCls} {
  922. filter: invert(54%) sepia(84%) saturate(431%) hue-rotate(139deg) brightness(97%) contrast(90%);
  923. transition: filter 0.2s;
  924. }
  925.  
  926. button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) .${iconColorCyanCls} {
  927. filter: invert(100%);
  928. }
  929.  
  930. .${iconColorGrayCls} {
  931. filter: invert(50%);
  932. transition: filter 0.2s;
  933. }
  934. button:has(.${reasoningModelCls}):hover .${iconColorGrayCls} {
  935. filter: invert(100%);
  936. }
  937.  
  938. .${iconColorWhiteCls} {
  939. filter: invert(50%);
  940. transition: filter 0.2s;
  941. }
  942. button:has(.${reasoningModelCls}):hover .${iconColorWhiteCls} {
  943. filter: invert(100%);
  944. }
  945.  
  946. .${iconColorGoldCls} {
  947. filter: brightness(0) saturate(100%) invert(88%) sepia(75%) saturate(2577%) hue-rotate(324deg) brightness(96%) contrast(99%);
  948. transition: filter 0.2s;
  949. }
  950.  
  951. `;
  952.  
  953. const TAG_POSITION = {
  954. BEFORE: 'before',
  955. AFTER: 'after',
  956. CARET: 'caret',
  957. };
  958.  
  959. const TAG_CONTAINER_TYPE = {
  960. NEW: 'new',
  961. NEW_IN_COLLECTION: 'new-in-collection',
  962. THREAD: 'thread',
  963. ALL: 'all',
  964. }
  965.  
  966. const tagsHelpText = `
  967. Each line is one tag.
  968. Non-field text is what will be inserted into prompt.
  969. Field is denoted by \`<\` and \`>\`, field name is before \`:\`, field value after \`:\`.
  970.  
  971. Supported fields:
  972. - \`label\`: tag label shown on tag "box" (new items around prompt input area)
  973. - \`position\`: where the tag text will be inserted, default is \`before\`; valid values are \`before\`/\`after\` (existing text) or \`caret\` (at cursor position)
  974. - \`color\`: tag color; CSS colors supported, you can use colors from a pre-generated palette via \`%\` syntax, e.g. \`<color:%5>\`. See palette bellow.
  975. - \`tooltip\`: shown on hover (aka title); (default) tooltip can be disabled when this field is set to empty string - \`<tooltip:>\`
  976. - \`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)
  977. - \`hide\`: hide the tag from the tag list
  978. - \`link\`: link to a URL, e.g. \`<link:https://example.com>\`, can be used for collections. only one link per tag is supported.
  979. - \`link-target\`: target of the link, e.g. \`<link-target:_blank>\` (opens in new tab), default is \`_self\` (same tab).
  980. - \`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.
  981. - \`set-mode\`: set the query mode: \`pro\` or \`deep-research\`, e.g. \`<set-mode:pro>\`
  982. - \`set-model\`: set the model, e.g. \`<set-model:claude-3-7-sonnet-thinking>\`
  983. - \`set-sources\`: set the sources, e.g. \`<set-sources:001>\` for disabled first source (web), disabled second source (academic), enabled third source (social)
  984. - \`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>\`
  985. - \`dir\`: unique identifier for a directory tag (it will not insert text into prompt)
  986. - \`in-dir\`: identifier of the parent directory this tag belongs to
  987. - \`fence\`: unique identifier for a fence definition (hidden by default)
  988. - \`in-fence\`: identifier of the fence this tag belongs to
  989. - \`fence-width\`: CSS width for a fence, e.g. \`<fence-width:10em>\`
  990. - \`fence-border-style\`: CSS border style for a fence (e.g., solid, dashed, dotted)
  991. - \`fence-border-color\`: CSS color or a palette \`%\` syntax for a fence border
  992. - \`fence-border-width\`: CSS width for a fence border
  993.  
  994. ---
  995.  
  996. | String | Replacement | Example |
  997. |---|---|---|
  998. | \`\\n\` | newline | |
  999. | \`$$time$$\` | current time | \`23:05\` |
  1000.  
  1001. ---
  1002.  
  1003. Examples:
  1004. \`\`\`
  1005. stable diffusion web ui - <label:SDWU>
  1006. , prefer concise modern syntax and style, <position:caret><label:concise modern>
  1007. tell me a joke<label:Joke><tooltip:>
  1008. tell me a joke<label:Joke & Submit><auto-submit>
  1009. \`\`\`
  1010.  
  1011. Directory example:
  1012. \`\`\`
  1013. <dir:games>Games<icon:gamepad-2>
  1014. <in-dir:games>FFXIV: <color:%15><label:FFXIV>
  1015. <in-dir:games>Vintage Story - <label:VS>
  1016. \`\`\`
  1017.  
  1018. Fence example:
  1019. \`\`\`
  1020. <fence:anime><fence-border-style:dashed><fence-border-color:%10>
  1021. <in-fence:anime>Shounen
  1022. <in-fence:anime>Seinen
  1023. <in-fence:anime>Shoujo
  1024. \`\`\`
  1025.  
  1026. Another fence example:
  1027. \`\`\`
  1028. <fence:programming><fence-border-style:solid><fence-border-color:%20>
  1029. <in-fence:programming>Haskell
  1030. <in-fence:programming>Raku<label:🦋>
  1031. \`\`\`
  1032. `.trim();
  1033.  
  1034. const defaultTagColor = '#282828';
  1035.  
  1036. const changeValueUsingEvent = (selector, value) => {
  1037. debugLog('changeValueUsingEvent', value, selector);
  1038.  
  1039. const nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  1040. nativeTextareaValueSetter.call(selector, value);
  1041. const inputEvent = new Event('input', {bubbles: true});
  1042. selector.dispatchEvent(inputEvent);
  1043. }
  1044.  
  1045. const TAGS_PALETTE_COLORS_NUM = 16;
  1046. const TAGS_PALETTE_CLASSIC = Object.freeze((() => {
  1047. const step = 360 / TAGS_PALETTE_COLORS_NUM;
  1048. const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  1049. return _.flow(
  1050. _.map(x => startH + x * step, _),
  1051. _.map(h => color2k.hsla(h, startS, startL, startA), _),
  1052. _.sortBy(x => color2k.parseToHsla(x)[0], _)
  1053. )(_.range(0, TAGS_PALETTE_COLORS_NUM));
  1054. })());
  1055.  
  1056. const TAGS_PALETTE_PASTEL = Object.freeze((() => {
  1057. const step = 360 / TAGS_PALETTE_COLORS_NUM;
  1058. const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  1059. return _.flow(
  1060. _.map(x => startH + x * step, _),
  1061. _.map(h => color2k.hsla(h, startS - 0.2, startL + 0.2, startA), _),
  1062. _.sortBy(x => color2k.parseToHsla(x)[0], _)
  1063. )(_.range(0, TAGS_PALETTE_COLORS_NUM));
  1064. })());
  1065.  
  1066. const TAGS_PALETTE_GRIM = Object.freeze((() => {
  1067. const step = 360 / TAGS_PALETTE_COLORS_NUM;
  1068. const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  1069. return _.flow(
  1070. _.map(x => startH + x * step, _),
  1071. _.map(h => color2k.hsla(h, startS - 0.6, startL - 0.3, startA), _),
  1072. _.sortBy(x => color2k.parseToHsla(x)[0], _)
  1073. )(_.range(0, TAGS_PALETTE_COLORS_NUM));
  1074. })());
  1075.  
  1076. const TAGS_PALETTE_DARK = Object.freeze((() => {
  1077. const step = 360 / TAGS_PALETTE_COLORS_NUM;
  1078. const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  1079. return _.flow(
  1080. _.map(x => startH + x * step, _),
  1081. _.map(h => color2k.hsla(h, startS, startL - 0.4, startA), _),
  1082. _.sortBy(x => color2k.parseToHsla(x)[0], _)
  1083. )(_.range(0, TAGS_PALETTE_COLORS_NUM));
  1084. })());
  1085.  
  1086. const TAGS_PALETTE_GRAY = Object.freeze((() => {
  1087. const step = 1 / TAGS_PALETTE_COLORS_NUM;
  1088. return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(0, 0, step * x, 1));
  1089. })());
  1090.  
  1091. const TAGS_PALETTE_CYAN = Object.freeze((() => {
  1092. const step = 1 / TAGS_PALETTE_COLORS_NUM;
  1093. const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  1094. return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(startH, startS, step * x, 1));
  1095. })());
  1096.  
  1097. const TAGS_PALETTE_TRANSPARENT = Object.freeze((() => {
  1098. const step = 1 / TAGS_PALETTE_COLORS_NUM;
  1099. return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(0, 0, 0, step * x));
  1100. })());
  1101.  
  1102. const TAGS_PALETTE_HACKER = Object.freeze((() => {
  1103. const step = 1 / TAGS_PALETTE_COLORS_NUM;
  1104. return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(120, step * x, step * x * 0.5, 1));
  1105. })());
  1106.  
  1107. const TAGS_PALETTES = Object.freeze({
  1108. CLASSIC: TAGS_PALETTE_CLASSIC,
  1109. PASTEL: TAGS_PALETTE_PASTEL,
  1110. GRIM: TAGS_PALETTE_GRIM,
  1111. DARK: TAGS_PALETTE_DARK,
  1112. GRAY: TAGS_PALETTE_GRAY,
  1113. CYAN: TAGS_PALETTE_CYAN,
  1114. TRANSPARENT: TAGS_PALETTE_TRANSPARENT,
  1115. HACKER: TAGS_PALETTE_HACKER,
  1116. CUSTOM: 'CUSTOM',
  1117. });
  1118.  
  1119. const convertColorInPaletteFormat = currentPalette => value => currentPalette[parseInt(dropStr(1)(value), 10)] ?? defaultTagColor;
  1120.  
  1121. const TAG_HOME_PAGE_LAYOUT = {
  1122. DEFAULT: 'default',
  1123. COMPACT: 'compact',
  1124. WIDER: 'wider',
  1125. WIDE: 'wide',
  1126. EXTRA_WIDE: 'extra-wide',
  1127. }
  1128.  
  1129. const parseBinaryState = binaryStr => {
  1130. if (!/^[01-]+$/.test(binaryStr)) {
  1131. throw new Error('Invalid binary state: ' + binaryStr);
  1132. }
  1133. return binaryStr.split('').map(bit => bit === '1' ? true : bit === '0' ? false : null);
  1134. };
  1135.  
  1136. const processTagField = currentPalette => name => value => {
  1137. if (name === 'color' && value.startsWith('%')) return convertColorInPaletteFormat(currentPalette)(value);
  1138. if (name === 'hide') return true;
  1139. if (name === 'auto-submit') return true;
  1140. if (name === 'set-sources') return parseBinaryState(value);
  1141. return value;
  1142. };
  1143.  
  1144. const tagLineRegex = /<(label|position|color|tooltip|target|hide|link|link-target|icon|dir|in-dir|fence|in-fence|fence-border-style|fence-border-color|fence-border-width|fence-width|set-mode|set-model|auto-submit|set-sources)(?::([^<>]*))?>/g;
  1145. const parseOneTagLine = currentPalette => line =>
  1146. Array.from(line.matchAll(tagLineRegex)).reduce(
  1147. (acc, match) => {
  1148. const [fullMatch, field, value] = match;
  1149. const processedValue = processTagField(currentPalette)(field)(value);
  1150. return {
  1151. ...acc,
  1152. [_.camelCase(field)]: processedValue,
  1153. text: acc.text.replace(fullMatch, '').replace(/\\n/g, '\n'),
  1154. };
  1155. },
  1156. {
  1157. text: line,
  1158. color: defaultTagColor,
  1159. target: TAG_CONTAINER_TYPE.NEW,
  1160. hide: false,
  1161. 'link-target': '_self',
  1162. }
  1163. );
  1164.  
  1165. const parseTagsText = text => {
  1166. const lines = text.split('\n').filter(tag => tag.trim().length > 0);
  1167. const palette = getPalette(loadConfig()?.tagPalette);
  1168. return lines.map(parseOneTagLine(palette)).map((x, i) => ({...x, originalIndex: i}));
  1169. };
  1170.  
  1171. const getTagsContainer = () => $c(tagsContainerCls);
  1172.  
  1173. const posFromTag = tag => Object.values(TAG_POSITION).includes(tag.position) ? tag.position : TAG_POSITION.BEFORE;
  1174.  
  1175. const applyTagToString = (tag, val, caretPos) => {
  1176. const {text} = tag;
  1177. const timeString = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
  1178. const processedText = text.replace(/\$\$time\$\$/g, timeString);
  1179. switch (posFromTag(tag)) {
  1180. case TAG_POSITION.BEFORE:
  1181. return `${processedText}${val}`;
  1182. case TAG_POSITION.AFTER:
  1183. return `${val}${processedText}`;
  1184. case TAG_POSITION.CARET:
  1185. return `${takeStr(caretPos)(val)}${processedText}${dropStr(caretPos)(val)}`;
  1186. default:
  1187. throw new Error(`Invalid position: ${tag.position}`);
  1188. }
  1189. };
  1190.  
  1191. const getPromptAreaFromTagsContainer = tagsContainerEl => PP.getAnyPromptArea(tagsContainerEl.parent());
  1192.  
  1193. const getPalette = paletteName => {
  1194. // Add this check for 'CUSTOM'
  1195. if (paletteName === TAGS_PALETTES.CUSTOM) {
  1196. // Use tagPaletteCustom from config or default if not found
  1197. return loadConfigOrDefault()?.tagPaletteCustom ?? defaultConfig.tagPaletteCustom;
  1198. }
  1199. // Fallback to predefined palettes or CLASSIC as default
  1200. const palette = TAGS_PALETTES[paletteName];
  1201. // Check if palette is an array before returning, otherwise return default
  1202. return Array.isArray(palette) ? palette : TAGS_PALETTES.CLASSIC;
  1203. }
  1204.  
  1205. const createTag = containerEl => isPreview => tag => {
  1206. if (tag.hide) return null;
  1207.  
  1208. const labelString = tag.label ?? tag.text;
  1209. const isTagLight = color2k.getLuminance(tag.color) > loadConfig().tagLuminanceThreshold;
  1210. const colorMod = isTagLight ? color2k.darken : color2k.lighten;
  1211. const hoverBgColor = color2k.toRgba(colorMod(tag.color, 0.1));
  1212. const borderColor = color2k.toRgba(colorMod(tag.color, loadConfig().tagTweakRichBorderColor ? 0.2 : 0.1));
  1213.  
  1214. const clickHandler = evt => {
  1215. debugLog('clicked', tag, evt);
  1216. if (tag.link) return;
  1217. if (tag.setMode) {
  1218. switch (tag.setMode) {
  1219. case 'pro':
  1220. PP.getModeProButton().click();
  1221. break;
  1222. case 'deep-research':
  1223. case 'dr':
  1224. PP.getModeDeepResearchButton().click();
  1225. break;
  1226. default:
  1227. throw new Error(`Invalid set-mode: ${tag.setMode}`);
  1228. }
  1229. }
  1230. if (tag.setModel) {
  1231. setTimeout(() => { // delay for model button to be available after setting mode
  1232. const modelDescriptor = PP.getModelDescriptorFromId(tag.setModel);
  1233. debugLog('[createTag] clickHandler: set model=', tag.setModel, ' modelDescriptor=', modelDescriptor);
  1234. PP.doSelectModel(modelDescriptor.index);
  1235. }, 50);
  1236. }
  1237. if (tag.setSources) {
  1238. setTimeout(() => {
  1239. PP.getAnySourcesButton().click();
  1240. setTimeout(() => {
  1241. PP.setSourcesSelectionListValues()(tag.setSources, {
  1242. callback: () => {
  1243. debugLogTags('[createTag] clickHandler: setSources callback');
  1244. setTimeout(() => {
  1245. PP.getAnySourcesButton().click();
  1246. }, 5);
  1247. },
  1248. });
  1249. debugLogTags('[createTag] clickHandler: setSources=', tag.setSources);
  1250. }, 10);
  1251. }, 80);
  1252. }
  1253. if (tag.autoSubmit) {
  1254. setTimeout(() => {
  1255. const submitButton = PP.submitButtonAny();
  1256. debugLogTags('[createTag] clickHandler: submitButton=', submitButton);
  1257. if (submitButton.length) {
  1258. if (submitButton.length > 1) {
  1259. debugLogTags('[createTag] clickHandler: multiple submit buttons found, using first one');
  1260. }
  1261. submitButton.first().click();
  1262. } else {
  1263. debugLogTags('[createTag] clickHandler: no submit button found');
  1264. }
  1265. }, 300);
  1266. }
  1267. const el = jq(evt.currentTarget);
  1268. const tagsContainer = el.closest(`.${tagsContainerCls}`);
  1269. if (!tagsContainer.length) {
  1270. debugLogTags('[clickHandler] no tags container found');
  1271. return;
  1272. }
  1273. const promptArea = getPromptAreaFromTagsContainer(tagsContainer);
  1274. if (!promptArea.length) {
  1275. debugLogTags('[clickHandler] no prompt area found', promptArea);
  1276. return;
  1277. }
  1278. const promptAreaRaw = promptArea[0];
  1279. const newText = applyTagToString(tag, promptArea.val(), promptAreaRaw.selectionStart);
  1280. changeValueUsingEvent(promptAreaRaw, newText);
  1281. promptAreaRaw.focus();
  1282. };
  1283.  
  1284. const tagFont = loadConfig().tagFont;
  1285.  
  1286. const defaultTooltip = tag.link? `${logPrefix} Open link: ${tag.link}` : `${logPrefix} Insert \`${tag.text}\` at position \`${posFromTag(tag)}\``;
  1287. const tagEl = jq(`<div/>`)
  1288. .addClass(tagCls)
  1289. .prop('title', tag.tooltip ?? defaultTooltip)
  1290. .attr('data-tag', JSON.stringify(tag))
  1291. .css({
  1292. backgroundColor: tag.color,
  1293. borderColor,
  1294. fontFamily: tagFont,
  1295. borderRadius: `${loadConfig().tagRoundness}px`,
  1296. })
  1297. .attr('data-color', color2k.toHex(tag.color))
  1298. .attr('data-hoverBgColor', color2k.toHex(hoverBgColor))
  1299. .attr('data-font', tagFont)
  1300. .on('mouseenter', event => {
  1301. jq(event.currentTarget).css('background-color', hoverBgColor);
  1302. })
  1303. .on('mouseleave', event => {
  1304. jq(event.currentTarget).css('background-color', tag.color);
  1305. });
  1306.  
  1307. if (isTagLight) {
  1308. tagEl.addClass(tagDarkTextCls);
  1309. }
  1310.  
  1311. if (loadConfig()?.tagTweakNoBorder) {
  1312. tagEl.addClass(tagTweakNoBorderCls);
  1313. }
  1314. if (loadConfig()?.tagTweakSlimPadding) {
  1315. tagEl.addClass(tagTweakSlimPaddingCls);
  1316. }
  1317. if (loadConfig()?.tagTweakTextShadow) {
  1318. tagEl.addClass(tagTweakTextShadowCls);
  1319. }
  1320.  
  1321. const textEl = jq('<span/>')
  1322. .text(labelString)
  1323. .css({
  1324. 'font-weight': loadConfig().tagBold ? 'bold' : 'normal',
  1325. 'font-style': loadConfig().tagItalic ? 'italic' : 'normal',
  1326. 'font-size': `${loadConfig().tagFontSize}px`,
  1327. 'transform': `translateY(${loadConfig().tagTextYOffset}px)`,
  1328. });
  1329.  
  1330. if (tag.icon) {
  1331. const iconEl = jq('<img/>')
  1332. .attr('src', getIconUrl(tag.icon))
  1333. .addClass(tagIconCls)
  1334. .css({
  1335. 'width': `${loadConfig().tagIconSize}px`,
  1336. 'height': `${loadConfig().tagIconSize}px`,
  1337. 'transform': `translateY(${loadConfig().tagIconYOffset}px)`,
  1338. });
  1339. if (!labelString) {
  1340. iconEl.css({
  1341. marginLeft: '0',
  1342. marginRight: '0',
  1343. });
  1344. }
  1345. textEl.prepend(iconEl);
  1346. }
  1347.  
  1348. tagEl.append(textEl);
  1349.  
  1350. if (tag.link) {
  1351. const linkEl = jq('<a/>')
  1352. .attr('href', tag.link)
  1353. .attr('target', tag.linkTarget)
  1354. .css({
  1355. textDecoration: 'none',
  1356. color: 'inherit'
  1357. });
  1358. textEl.wrap(linkEl);
  1359. }
  1360.  
  1361. if (!isPreview && !tag.link && !tag.dir) {
  1362. tagEl.click(clickHandler);
  1363. }
  1364. containerEl.append(tagEl);
  1365.  
  1366. return tagEl;
  1367. };
  1368.  
  1369. const genDebugFakeTags = () =>
  1370. _.times(TAGS_PALETTE_COLORS_NUM, x => `Fake ${x} ${_.times(x / 3).map(() => 'x').join('')}<color:%${x % TAGS_PALETTE_COLORS_NUM}>`)
  1371. .join('\n');
  1372.  
  1373. const getTagContainerType = containerEl => {
  1374. if (containerEl.hasClass(threadTagContainerCls) || containerEl.hasClass(tagsPreviewThreadCls)) return TAG_CONTAINER_TYPE.THREAD;
  1375. if (containerEl.hasClass(newTagContainerCls) || containerEl.hasClass(tagsPreviewNewCls)) return TAG_CONTAINER_TYPE.NEW;
  1376. if (containerEl.hasClass(newTagContainerInCollectionCls) || containerEl.hasClass(tagsPreviewNewInCollectionCls)) return TAG_CONTAINER_TYPE.NEW_IN_COLLECTION;
  1377. return null;
  1378. }
  1379.  
  1380. const getPromptWrapperTagContainerType = promptWrapper => {
  1381. if (PP.getPromptAreaOfNewThread(promptWrapper).length) return TAG_CONTAINER_TYPE.NEW;
  1382. if (PP.getPromptAreaOnThread(promptWrapper).length) return TAG_CONTAINER_TYPE.THREAD;
  1383. if (PP.getPromptAreaOnCollection(promptWrapper).length) return TAG_CONTAINER_TYPE.NEW_IN_COLLECTION;
  1384. return null;
  1385. };
  1386.  
  1387. const isTagRelevantForContainer = containerType => tag =>
  1388. containerType === tag.target
  1389. || (containerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION && tag.target === TAG_CONTAINER_TYPE.NEW)
  1390. || tag.target === TAG_CONTAINER_TYPE.ALL
  1391.  
  1392. const tagContainerTypeToTagContainerClass = {
  1393. [TAG_CONTAINER_TYPE.THREAD]: threadTagContainerCls,
  1394. [TAG_CONTAINER_TYPE.NEW]: newTagContainerCls,
  1395. [TAG_CONTAINER_TYPE.NEW_IN_COLLECTION]: newTagContainerInCollectionCls,
  1396. };
  1397.  
  1398. const currentUrlIsSettingsPage = () => window.location.pathname.includes('/settings/');
  1399.  
  1400. const refreshTags = ({force = false} = {}) => {
  1401. const promptWrapper = PP.getPromptAreaWrapperOfNewThread()
  1402. .add(PP.getPromptAreaWrapperOnThread())
  1403. .add(PP.getPromptAreaWrapperOnCollection())
  1404. .filter((_, rEl) => {
  1405. const isPreview = Boolean(jq(rEl).attr('data-preview'));
  1406. return isPreview || !currentUrlIsSettingsPage();
  1407. });
  1408. if (!promptWrapper.length) {
  1409. debugLogTags('no prompt area found');
  1410. }
  1411. // debugLogTags('promptWrappers', promptWrapper);
  1412. const allTags = _.flow(
  1413. x => x + (unsafeWindow.phFakeTags ? `${nl}${genDebugFakeTags()}${nl}` : ''),
  1414. parseTagsText,
  1415. )(loadConfig()?.tagsText ?? defaultConfig.tagsText);
  1416. debugLogTags('refreshing allTags', allTags);
  1417.  
  1418. const createContainer = (promptWrapper) => {
  1419. const el = jq(`<div/>`).addClass(tagsContainerCls);
  1420. const tagContainerType = getPromptWrapperTagContainerType(promptWrapper);
  1421. if (tagContainerType) {
  1422. const clsToAdd = tagContainerTypeToTagContainerClass[tagContainerType];
  1423. if (!clsToAdd) {
  1424. console.error('Unexpected tagContainerType:', tagContainerType, {promptWrapper});
  1425. }
  1426. el.addClass(clsToAdd);
  1427. }
  1428. return el;
  1429. }
  1430. promptWrapper.each((_, rEl) => {
  1431. const el = jq(rEl);
  1432. if (el.parent().find(`.${tagsContainerCls}`).length) {
  1433. el.parent().addClass(queryBoxCls);
  1434. return;
  1435. }
  1436. el.before(createContainer(el));
  1437. });
  1438.  
  1439. const currentPalette = getPalette(loadConfig().tagPalette);
  1440.  
  1441. const createFence = (fence) => {
  1442. const fenceEl = jq('<div/>')
  1443. .addClass(tagFenceCls)
  1444. .css({
  1445. 'border-style': fence.fenceBorderStyle ?? 'solid',
  1446. 'border-color': fence.fenceBorderColor?.startsWith('%')
  1447. ? convertColorInPaletteFormat(currentPalette)(fence.fenceBorderColor)
  1448. : fence.fenceBorderColor ?? defaultTagColor,
  1449. 'border-width': fence.fenceBorderWidth ?? '1px',
  1450. })
  1451. .attr('data-tag', JSON.stringify(fence))
  1452. ;
  1453. const fenceContentEl = jq('<div/>')
  1454. .addClass(tagFenceContentCls)
  1455. .css({
  1456. 'width': fence.fenceWidth ?? '',
  1457. })
  1458. ;
  1459. fenceEl.append(fenceContentEl);
  1460. return { fenceEl, fenceContentEl };
  1461. };
  1462.  
  1463. const createDirectory = () => {
  1464. const directoryEl = jq('<div/>').addClass(tagDirectoryCls);
  1465. const directoryContentEl = jq('<div/>').addClass(tagDirectoryContentCls);
  1466. directoryEl.append(directoryContentEl);
  1467. return { directoryEl, directoryContentEl };
  1468. };
  1469.  
  1470. const containerEls = getTagsContainer();
  1471. containerEls.each((_i, rEl) => {
  1472. const containerEl = jq(rEl);
  1473. const isPreview = Boolean(containerEl.attr('data-preview'));
  1474.  
  1475. const tagContainerTypeFromPromptWrapper = getPromptWrapperTagContainerType(containerEl.nthParent(2));
  1476. const prelimTagContainerType = getTagContainerType(containerEl);
  1477. if (tagContainerTypeFromPromptWrapper !== prelimTagContainerType && !isPreview) {
  1478. debugLog('tagContainerTypeFromPromptWrapper !== prelimTagContainerType', {tagContainerTypeFromPromptWrapper, prelimTagContainerType, containerEl, isPreview});
  1479. containerEl
  1480. .empty()
  1481. .removeClass(threadTagContainerCls, newTagContainerCls, newTagContainerInCollectionCls)
  1482. .addClass(tagContainerTypeToTagContainerClass[tagContainerTypeFromPromptWrapper])
  1483. ;
  1484. } else {
  1485. if (!isPreview) {
  1486. debugLogTags('tagContainerTypeFromPromptWrapper === prelimTagContainerType', {tagContainerTypeFromPromptWrapper, prelimTagContainerType, containerEl, isPreview});
  1487. }
  1488. }
  1489.  
  1490. // TODO: use something else than lodash/fp. in following functions it behaved randomly very weirdly
  1491. // e.g. partial application of map resulting in an empty array or sortBy sorting field name instead
  1492. // of input array. possibly inconsistent normal FP order of arguments
  1493. const mapParseAttrTag = xs => xs.map(el => JSON.parse(el.dataset.tag));
  1494. const sortByOriginalIndex = xs => [...xs].sort((a, b) => a.originalIndex - b.originalIndex);
  1495. const tagElsInCurrentContainer = containerEl.find(`.${tagCls}, .${tagFenceCls}`).toArray();
  1496. const filterOutHidden = filter(x => !x.hide);
  1497. const currentTags = _.flow(
  1498. mapParseAttrTag,
  1499. sortByOriginalIndex,
  1500. filterOutHidden,
  1501. _.uniq,
  1502. )(tagElsInCurrentContainer);
  1503. const tagContainerType = getTagContainerType(containerEl);
  1504. const tagsForThisContainer = _.flow(
  1505. filter(isTagRelevantForContainer(tagContainerType)),
  1506. filterOutHidden,
  1507. sortByOriginalIndex,
  1508. )(allTags);
  1509. debugLogTags('tagContainerType =', tagContainerType, ', current tags =', currentTags, ', tagsForThisContainer =', tagsForThisContainer, ', tagElsInCurrentContainer =', tagElsInCurrentContainer);
  1510. if (_.isEqual(currentTags, tagsForThisContainer) && !force) {
  1511. debugLogTags('no tags changed');
  1512. return;
  1513. }
  1514. const diff = jsondiffpatch.diff(currentTags, tagsForThisContainer);
  1515. const changedTags = jsondiffpatch.formatters.console.format(diff);
  1516. debugLogTags('changedTags', changedTags);
  1517. containerEl.empty();
  1518. const tagHomePageLayout = loadConfig()?.tagHomePageLayout;
  1519. if (!isPreview) {
  1520. if ((tagContainerType === TAG_CONTAINER_TYPE.NEW || tagContainerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION)) {
  1521. if (tagContainerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION) {
  1522. // only compact layout is supported for new in collection
  1523. if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) {
  1524. containerEl.addClass(tagContainerCompactCls);
  1525. }
  1526. } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) {
  1527. containerEl.addClass(tagContainerCompactCls);
  1528. } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDER) {
  1529. containerEl.addClass(tagContainerWiderCls);
  1530. } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDE) {
  1531. containerEl.addClass(tagContainerWideCls);
  1532. } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.EXTRA_WIDE) {
  1533. containerEl.addClass(tagContainerExtraWideCls);
  1534. } else {
  1535. containerEl.removeClass(`${tagContainerCompactCls} ${tagContainerWiderCls} ${tagContainerWideCls} ${tagContainerExtraWideCls}`);
  1536. }
  1537. }
  1538. }
  1539.  
  1540. const fences = {};
  1541. const directories = {};
  1542.  
  1543. const fencesWrapperEl = jq('<div/>').addClass(tagAllFencesWrapperCls);
  1544. const restWrapperEl = jq('<div/>').addClass(tagRestOfTagsWrapperCls);
  1545.  
  1546. tagsForThisContainer.forEach(tag => {
  1547. const { fence, dir, inFence, inDir } = tag;
  1548. const getOrCreateDirectory = dirName => {
  1549. if (!directories[dirName]) directories[dirName] = createDirectory();
  1550. return directories[dirName];
  1551. };
  1552. const getTagContainer = () => {
  1553. if (fence) {
  1554. if (!fences[fence]) fences[fence] = createFence(tag);
  1555. return fences[fence].fenceContentEl;
  1556. } else if (dir && inFence) {
  1557. if (!fences[inFence]) {
  1558. console.error(`fence ${inFence} for tag not found`, tag);
  1559. return null;
  1560. }
  1561. const { directoryEl } = getOrCreateDirectory(dir);
  1562. fences[inFence].fenceContentEl.append(directoryEl);
  1563. return directoryEl;
  1564. } else if (dir) {
  1565. const { directoryEl } = getOrCreateDirectory(dir);
  1566. restWrapperEl.append(directoryEl);
  1567. return directoryEl;
  1568. } else if (inFence) {
  1569. if (!fences[inFence]) {
  1570. console.error(`fence ${inFence} for tag not found`, tag);
  1571. return null;
  1572. }
  1573. return fences[inFence].fenceContentEl;
  1574. } else if (inDir) {
  1575. if (!directories[inDir]) {
  1576. console.error(`directory ${inDir} for tag not found`, tag);
  1577. return null;
  1578. }
  1579. return directories[inDir].directoryContentEl;
  1580. } else {
  1581. return restWrapperEl;
  1582. }
  1583. };
  1584. const tagContainer = getTagContainer();
  1585. if (tagContainer && !fence) {
  1586. createTag(tagContainer)(isPreview)(tag);
  1587. }
  1588. });
  1589.  
  1590. Object.values(fences).forEach(({ fenceEl }) => fencesWrapperEl.append(fenceEl));
  1591. containerEl.append(fencesWrapperEl).append(restWrapperEl);
  1592. });
  1593. }
  1594.  
  1595. const setupTags = () => {
  1596. debugLog('setting up tags');
  1597. setInterval(refreshTags, 500);
  1598. }
  1599.  
  1600. const ICON_REPLACEMENT_MODE = Object.freeze({
  1601. OFF: 'Off',
  1602. LUCIDE1: 'Lucide 1',
  1603. LUCIDE2: 'Lucide 2',
  1604. LUCIDE3: 'Lucide 3',
  1605. TDESIGN1: 'TDesign 1',
  1606. TDESIGN2: 'TDesign 2',
  1607. TDESIGN3: 'TDesign 3',
  1608. });
  1609.  
  1610. const leftPanelIconMappingsToLucide1 = Object.freeze({
  1611. 'search': 'search',
  1612. 'discover': 'telescope',
  1613. 'collection-2': 'shapes',
  1614. 'library': 'library-big',
  1615. });
  1616.  
  1617. const leftPanelIconMappingsToLucide2 = Object.freeze({
  1618. 'search': 'house',
  1619. 'discover': 'compass',
  1620. 'collection-2': 'square-stack',
  1621. 'library': 'archive',
  1622. });
  1623.  
  1624. const leftPanelIconMappingsToLucide3 = Object.freeze({
  1625. 'search': 'search',
  1626. 'discover': 'telescope',
  1627. 'collection-2': 'bot',
  1628. 'library': 'folder-open',
  1629. });
  1630.  
  1631. const leftPanelIconMappingsToTDesign1 = Object.freeze({
  1632. 'search': 'search',
  1633. 'discover': 'compass-filled',
  1634. 'collection-2': 'grid-view',
  1635. 'library': 'book',
  1636. });
  1637.  
  1638. const leftPanelIconMappingsToTDesign2 = Object.freeze({
  1639. 'search': 'search',
  1640. 'discover': 'shutter-filled',
  1641. 'collection-2': 'palette-1',
  1642. 'library': 'folder-open-1-filled',
  1643. });
  1644.  
  1645. const leftPanelIconMappingsToTDesign3 = Object.freeze({
  1646. 'search': 'search',
  1647. 'discover': 'banana-filled',
  1648. 'collection-2': 'chili-filled',
  1649. 'library': 'barbecue-filled',
  1650. });
  1651.  
  1652. const iconMappings = {
  1653. LUCIDE1: leftPanelIconMappingsToLucide1,
  1654. LUCIDE2: leftPanelIconMappingsToLucide2,
  1655. LUCIDE3: leftPanelIconMappingsToLucide3,
  1656. TDESIGN1: leftPanelIconMappingsToTDesign1,
  1657. TDESIGN2: leftPanelIconMappingsToTDesign2,
  1658. TDESIGN3: leftPanelIconMappingsToTDesign3,
  1659. };
  1660.  
  1661. const MODEL_LABEL_TEXT_MODE = Object.freeze({
  1662. OFF: 'Off',
  1663. FULL_NAME: 'Full Name',
  1664. SHORT_NAME: 'Short Name',
  1665. PP_MODEL_ID: 'PP Model ID',
  1666. OWN_NAME_VERSION_SHORT: 'Own Name + Version Short',
  1667. });
  1668.  
  1669. const MODEL_LABEL_STYLE = Object.freeze({
  1670. OFF: 'Off',
  1671. JUST_TEXT: 'Just Text',
  1672. BUTTON_SUBTLE: 'Button Subtle',
  1673. BUTTON_WHITE: 'Button White',
  1674. BUTTON_CYAN: 'Button Cyan',
  1675. });
  1676.  
  1677. const defaultConfig = Object.freeze({
  1678. showCopilot: true,
  1679. showCopilotNewThread: true,
  1680. showCopilotRepeatLast: true,
  1681. showCopilotCopyPlaceholder: true,
  1682. tagsText: '',
  1683. debugMode: false,
  1684. debugTagsMode: false,
  1685. tagPalette: 'CLASSIC',
  1686. tagPaletteCustom: ['#000', '#fff', '#ff0', '#f00', '#0f0', '#00f', '#0ff', '#f0f'],
  1687. tagFont: 'Roboto',
  1688. tagHomePageLayout: TAG_HOME_PAGE_LAYOUT.DEFAULT,
  1689. tagLuminanceThreshold: 0.35,
  1690. tagBold: false,
  1691. tagItalic: false,
  1692. tagFontSize: 16,
  1693. tagIconSize: 16,
  1694. tagRoundness: 4,
  1695. tagTextYOffset: 0,
  1696. tagIconYOffset: 0,
  1697. replaceIconsInMenu: ICON_REPLACEMENT_MODE.OFF,
  1698. slimLeftMenu: false,
  1699. hideHomeWidgets: false,
  1700. hideDiscoverButton: false,
  1701. fixImageGenerationOverlay: false,
  1702. extraSpaceBellowLastAnswer: false,
  1703. modelLabelTextMode: MODEL_LABEL_TEXT_MODE.OFF,
  1704. modelLabelStyle: MODEL_LABEL_STYLE.OFF,
  1705. modelLabelOverwriteCyanIconToGray: false,
  1706. modelLabelUseIconForReasoningModels: false,
  1707. modelLabelReasoningModelIconGold: false,
  1708. customModelPopover: false,
  1709. mainCaptionHtml: '',
  1710. });
  1711.  
  1712. // TODO: if still using local storage, at least it should be prefixed with user script name
  1713. const storageKey = 'checkBoxStates';
  1714.  
  1715. const loadConfig = () => {
  1716. try {
  1717. // TODO: use storage from GM API
  1718. const val = JSON.parse(localStorage.getItem(storageKey));
  1719. // debugLog('loaded config', val);
  1720. return val;
  1721. } catch (e) {
  1722. console.error('Failed to load config, using default', e);
  1723. return defaultConfig;
  1724. }
  1725. }
  1726.  
  1727. const loadConfigOrDefault = () => loadConfig() ?? defaultConfig
  1728.  
  1729. const saveConfig = cfg => {
  1730. debugLog('saving config', cfg);
  1731. localStorage.setItem(storageKey, JSON.stringify(cfg));
  1732. };
  1733.  
  1734. const createCheckbox = (id, labelText, onChange) => {
  1735. debugLog("createCheckbox", id);
  1736. const checkbox = jq(`<input type="checkbox" id=${id}>`);
  1737. const label = jq(`<label class="checkbox_label" for="${id}">${labelText}</label>`);
  1738. const checkboxWithLabel = jq('<div class="checkbox_wrapper"></div>').append(checkbox).append(' ').append(label);
  1739. debugLog('checkboxwithlabel', checkboxWithLabel);
  1740.  
  1741. getSettingsLastTabGroupContent().append(checkboxWithLabel);
  1742. checkbox.on('change', onChange);
  1743. return checkbox;
  1744. };
  1745.  
  1746. const createTextArea = (id, labelText, onChange, helpText, links) => {
  1747. debugLog("createTextArea", id);
  1748. const textarea = jq(`<textarea id=${id}></textarea>`);
  1749. const bookIconHtml = `<img src="${getLucideIconUrl('book-text')}" class="w-4 h-4 invert inline-block"/>`;
  1750. const labelTextHtml = `<span class="opacity-100">${labelText}</span>`;
  1751. const label = jq(`<label class="textarea_label">${labelTextHtml}${helpText ? ' ' + bookIconHtml : ''}</label>`);
  1752. const labelWithLinks = jq('<div/>').addClass('flex flex-row gap-2 mb-2').append(label);
  1753. const textareaWrapper = jq('<div class="textarea_wrapper"></div>').append(labelWithLinks);
  1754. if (links) {
  1755. links.forEach(({icon, label, url, tooltip}) => {
  1756. const iconHtml = `<img src="${getIconUrl(icon)}" class="w-4 h-4 invert opacity-50 hover:opacity-100 transition-opacity duration-300 ease-in-out"/>`;
  1757. const link = jq(`<a href="${url}" target="_blank" class="flex flex-row gap-2 items-center">${icon ? iconHtml : ''}${label ? ' ' + label : ''}</a>`);
  1758. link.attr('title', tooltip);
  1759. labelWithLinks.append(link);
  1760. });
  1761. }
  1762. if (helpText) {
  1763. const help = jq(`<div/>`).addClass(helpTextCls).html(markdownConverter.makeHtml(helpText)).append(jq('<br/>'));
  1764. help.find('a').each((_, a) => jq(a).attr('target', '_blank'));
  1765. help.append(jq('<button/>').text('[Close help]').on('click', () => help.hide()));
  1766. textareaWrapper.append(help);
  1767. label
  1768. .css({cursor: 'pointer'})
  1769. .on('click', () => help.toggle())
  1770. .prop('title', 'Click to toggle help')
  1771. ;
  1772. help.hide();
  1773. }
  1774. textareaWrapper.append(textarea);
  1775. debugLog('textareaWithLabel', textareaWrapper);
  1776.  
  1777. getSettingsLastTabGroupContent().append(textareaWrapper);
  1778. textarea.on('change', onChange);
  1779. return textarea;
  1780. };
  1781.  
  1782. const createSelect = (id, labelText, options, onChange) => {
  1783. const select = jq(`<select id=${id}>`);
  1784. options.forEach(({value, label}) => {
  1785. jq('<option>').val(value).text(label).appendTo(select);
  1786. });
  1787. const label = jq(`<label class="select_label">${labelText}</label>`);
  1788. const selectWithLabel = jq('<div class="select_wrapper"></div>').append(select).append(label);
  1789. debugLog('selectWithLabel', selectWithLabel);
  1790.  
  1791. getSettingsLastTabGroupContent().append(selectWithLabel);
  1792. select.on('change', onChange);
  1793. return select;
  1794. };
  1795.  
  1796. const createPaletteLegend = paletteName => {
  1797. const wrapper = jq('<div/>')
  1798. .addClass(tagPaletteCls)
  1799. .append(jq('<span>').html('Palette of color codes:&nbsp;'))
  1800. ;
  1801. const palette = getPalette(paletteName);
  1802. palette.forEach((color, i) => {
  1803. const colorCode = `%${i}`;
  1804. const colorPart = genColorPart(colorCode);
  1805. // console.log('createPaletteLegend', {i, colorCode, colorPart, color});
  1806. jq('<span/>')
  1807. .text(colorCode)
  1808. .addClass(tagPaletteItemCls)
  1809. .css({
  1810. 'background-color': color,
  1811. })
  1812. .prop('title', `Copy ${colorPart} to clipboard`)
  1813. .click(() => {
  1814. copyTextToClipboard(colorPart);
  1815. })
  1816. .appendTo(wrapper);
  1817. });
  1818. return wrapper;
  1819. }
  1820.  
  1821. const createColorInput = (id, labelText, onChange) => {
  1822. debugLog("createColorInput", id);
  1823. const input = jq(`<input type="color" id=${id}>`);
  1824. const label = jq(`<label class="color_label">${labelText}</label>`);
  1825. const inputWithLabel = jq('<div class="color_wrapper"></div>').append(input).append(label);
  1826. debugLog('inputWithLabel', inputWithLabel);
  1827.  
  1828. getSettingsLastTabGroupContent().append(inputWithLabel);
  1829. input.on('change', onChange);
  1830. return input;
  1831. }
  1832.  
  1833. const createNumberInput = (id, labelText, onChange, {step = 1, min = 0, max = 100} = {}) => {
  1834. debugLog("createNumberInput", id);
  1835. const input = jq(`<input type="number" id=${id}>`)
  1836. .prop('step', step)
  1837. .prop('min', min)
  1838. .prop('max', max)
  1839. ;
  1840. const label = jq(`<label class="number_label">${labelText}</label>`);
  1841. const inputWithLabel = jq('<div class="number_wrapper"></div>').append(input).append(label);
  1842. debugLog('inputWithLabel', inputWithLabel);
  1843.  
  1844. getSettingsLastTabGroupContent().append(inputWithLabel);
  1845. input.on('change', onChange);
  1846. return input;
  1847. };
  1848.  
  1849. const createTagsPreview = () => {
  1850. const wrapper = jq('<div/>')
  1851. .addClass(tagsPreviewCls)
  1852. .append(jq('<div>').text('Preview').addClass('text-lg font-bold'))
  1853. .append(jq('<div>').text('Target New:'))
  1854. .append(jq('<div>').addClass(tagsPreviewNewCls).addClass(tagsContainerCls).attr('data-preview', 'true'))
  1855. .append(jq('<div>').text('Target Thread:'))
  1856. .append(jq('<div>').addClass(tagsPreviewThreadCls).addClass(tagsContainerCls).attr('data-preview', 'true'))
  1857. ;
  1858. getSettingsLastTabGroupContent().append(wrapper);
  1859. }
  1860.  
  1861. const coPilotNewThreadAutoSubmitCheckboxId = 'coPilotNewThreadAutoSubmit';
  1862. const getCoPilotNewThreadAutoSubmitCheckbox = () => $i(coPilotNewThreadAutoSubmitCheckboxId);
  1863.  
  1864. const coPilotRepeatLastAutoSubmitCheckboxId = 'coPilotRepeatLastAutoSubmit';
  1865. const getCoPilotRepeatLastAutoSubmitCheckbox = () => $i(coPilotRepeatLastAutoSubmitCheckboxId);
  1866.  
  1867. const hideSideMenuCheckboxId = 'hideSideMenu';
  1868. const getHideSideMenuCheckbox = () => $i(hideSideMenuCheckboxId);
  1869.  
  1870. const tagsTextAreaId = 'tagsText';
  1871. const getTagsTextArea = () => $i(tagsTextAreaId);
  1872.  
  1873. const tagColorPickerId = genCssName('tagColorPicker');
  1874. const getTagColorPicker = () => $i(tagColorPickerId);
  1875.  
  1876. const enableDebugCheckboxId = genCssName('enableDebug');
  1877. const getEnableDebugCheckbox = () => $i(enableDebugCheckboxId);
  1878.  
  1879. const enableTagsDebugCheckboxId = genCssName('enableTagsDebug');
  1880. const getEnableTagsDebugCheckbox = () => $i(enableTagsDebugCheckboxId);
  1881.  
  1882. const tagPaletteSelectId = genCssName('tagPaletteSelect');
  1883. const getTagPaletteSelect = () => $i(tagPaletteSelectId);
  1884.  
  1885. const tagFontSelectId = genCssName('tagFontSelect');
  1886. const getTagFontSelect = () => $i(tagFontSelectId);
  1887.  
  1888. const tagTweakNoBorderCheckboxId = genCssName('tagTweakNoBorder');
  1889. const getTagTweakNoBorderCheckbox = () => $i(tagTweakNoBorderCheckboxId);
  1890.  
  1891. const tagTweakSlimPaddingCheckboxId = genCssName('tagTweakSlimPadding');
  1892. const getTagTweakSlimPaddingCheckbox = () => $i(tagTweakSlimPaddingCheckboxId);
  1893.  
  1894. const tagTweakRichBorderColorCheckboxId = genCssName('tagTweakRichBorderColor');
  1895. const getTagTweakRichBorderColorCheckbox = () => $i(tagTweakRichBorderColorCheckboxId);
  1896.  
  1897. const tagTweakTextShadowCheckboxId = genCssName('tagTweakTextShadow');
  1898. const getTagTweakTextShadowCheckbox = () => $i(tagTweakTextShadowCheckboxId);
  1899.  
  1900. const tagHomePageLayoutSelectId = genCssName('tagHomePageLayout');
  1901. const getTagHomePageLayoutSelect = () => $i(tagHomePageLayoutSelectId);
  1902.  
  1903. const tagLuminanceThresholdInputId = genCssName('tagLuminanceThreshold');
  1904. const getTagLuminanceThresholdInput = () => $i(tagLuminanceThresholdInputId);
  1905.  
  1906. const tagBoldCheckboxId = genCssName('tagBold');
  1907. const getTagBoldCheckbox = () => $i(tagBoldCheckboxId);
  1908.  
  1909. const tagItalicCheckboxId = genCssName('tagItalic');
  1910. const getTagItalicCheckbox = () => $i(tagItalicCheckboxId);
  1911.  
  1912. const tagFontSizeInputId = genCssName('tagFontSize');
  1913. const getTagFontSizeInput = () => $i(tagFontSizeInputId);
  1914.  
  1915. const tagIconSizeInputId = genCssName('tagIconSize');
  1916. const getTagIconSizeInput = () => $i(tagIconSizeInputId);
  1917.  
  1918. const tagRoundnessInputId = genCssName('tagRoundness');
  1919. const getTagRoundnessInput = () => $i(tagRoundnessInputId);
  1920.  
  1921. const tagTextYOffsetInputId = genCssName('tagTextYOffset');
  1922. const getTagTextYOffsetInput = () => $i(tagTextYOffsetInputId);
  1923.  
  1924. const tagIconYOffsetInputId = genCssName('tagIconYOffset');
  1925. const getTagIconYOffsetInput = () => $i(tagIconYOffsetInputId);
  1926.  
  1927. const tagPaletteCustomTextAreaId = genCssName('tagPaletteCustomTextArea');
  1928. const getTagPaletteCustomTextArea = () => $i(tagPaletteCustomTextAreaId);
  1929.  
  1930. const replaceIconsInMenuId = genCssName('replaceIconsInMenu');
  1931. const getReplaceIconsInMenu = () => $i(replaceIconsInMenuId);
  1932.  
  1933. const slimLeftMenuCheckboxId = genCssName('slimLeftMenu');
  1934. const getSlimLeftMenuCheckbox = () => $i(slimLeftMenuCheckboxId);
  1935.  
  1936. const hideHomeWidgetsCheckboxId = genCssName('hideHomeWidgets');
  1937. const getHideHomeWidgetsCheckbox = () => $i(hideHomeWidgetsCheckboxId);
  1938.  
  1939. const hideDiscoverButtonCheckboxId = genCssName('hideDiscoverButton');
  1940. const getHideDiscoverButtonCheckbox = () => $i(hideDiscoverButtonCheckboxId);
  1941.  
  1942. const fixImageGenerationOverlayCheckboxId = genCssName('fixImageGenerationOverlay');
  1943. const getFixImageGenerationOverlayCheckbox = () => $i(fixImageGenerationOverlayCheckboxId);
  1944.  
  1945. const extraSpaceBellowLastAnswerCheckboxId = genCssName('extraSpaceBellowLastAnswer');
  1946. const getExtraSpaceBellowLastAnswerCheckbox = () => $i(extraSpaceBellowLastAnswerCheckboxId);
  1947.  
  1948. const modelLabelTextModeSelectId = genCssName('modelLabelTextModeSelect');
  1949. const getModelLabelTextModeSelect = () => $i(modelLabelTextModeSelectId);
  1950.  
  1951. const modelLabelStyleSelectId = genCssName('modelLabelStyleSelect');
  1952. const getModelLabelStyleSelect = () => $i(modelLabelStyleSelectId);
  1953.  
  1954. const modelLabelOverwriteCyanIconToGrayCheckboxId = genCssName('modelLabelOverwriteCyanIconToGray');
  1955. const getModelLabelOverwriteCyanIconToGrayCheckbox = () => $i(modelLabelOverwriteCyanIconToGrayCheckboxId);
  1956.  
  1957. const modelLabelUseIconForReasoningModelsCheckboxId = genCssName('modelLabelUseIconForReasoningModels');
  1958. const getModelLabelUseIconForReasoningModelsCheckbox = () => $i(modelLabelUseIconForReasoningModelsCheckboxId);
  1959.  
  1960. const modelLabelReasoningModelIconGoldCheckboxId = genCssName('modelLabelReasoningModelIconGold');
  1961. const getModelLabelReasoningModelIconGoldCheckbox = () => $i(modelLabelReasoningModelIconGoldCheckboxId);
  1962.  
  1963. const customModelPopoverCheckboxId = genCssName('customModelPopoverCheckbox');
  1964. const getCustomModelPopoverCheckbox = () => $i(customModelPopoverCheckboxId);
  1965.  
  1966. const mainCaptionHtmlTextAreaId = genCssName('mainCaptionHtmlTextArea');
  1967. const getMainCaptionHtmlTextArea = () => $i(mainCaptionHtmlTextAreaId);
  1968.  
  1969. const copyTextToClipboard = async text => {
  1970. try {
  1971. await navigator.clipboard.writeText(text);
  1972. console.log('Text copied to clipboard', {text});
  1973. } catch (err) {
  1974. console.error('Failed to copy text: ', err);
  1975. }
  1976. };
  1977.  
  1978. const genColorPart = color => `<color:${color}>`;
  1979.  
  1980. const loadCurrentConfigToSettingsForm = () => {
  1981. const savedStatesRaw = JSON.parse(localStorage.getItem(storageKey));
  1982. if (savedStatesRaw === null) { return; }
  1983. const savedStates = {...defaultConfig, ...savedStatesRaw};
  1984.  
  1985. getCoPilotNewThreadAutoSubmitCheckbox().prop('checked', savedStates.coPilotNewThreadAutoSubmit);
  1986. getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked', savedStates.coPilotRepeatLastAutoSubmit);
  1987. getHideSideMenuCheckbox().prop('checked', savedStates.hideSideMenu);
  1988. getTagsTextArea().val(savedStates.tagsText);
  1989. getEnableDebugCheckbox().prop('checked', savedStates.enableDebug);
  1990. getEnableTagsDebugCheckbox().prop('checked', savedStates.debugTagsMode);
  1991. getTagPaletteSelect().val(savedStates.tagPalette);
  1992. getTagFontSelect().val(savedStates.tagFont);
  1993. getTagTweakNoBorderCheckbox().prop('checked', savedStates.tagTweakNoBorder);
  1994. getTagTweakSlimPaddingCheckbox().prop('checked', savedStates.tagTweakSlimPadding);
  1995. getTagTweakRichBorderColorCheckbox().prop('checked', savedStates.tagTweakRichBorderColor);
  1996. getTagTweakTextShadowCheckbox().prop('checked', savedStates.tagTweakTextShadow);
  1997. getTagHomePageLayoutSelect().val(savedStates.tagHomePageLayout);
  1998. getTagLuminanceThresholdInput().val(savedStates.tagLuminanceThreshold);
  1999. getTagBoldCheckbox().prop('checked', savedStates.tagBold);
  2000. getTagItalicCheckbox().prop('checked', savedStates.tagItalic);
  2001. getTagFontSizeInput().val(savedStates.tagFontSize);
  2002. getTagIconSizeInput().val(savedStates.tagIconSize);
  2003. getTagTextYOffsetInput().val(savedStates.tagTextYOffset);
  2004. getTagIconYOffsetInput().val(savedStates.tagIconYOffset);
  2005. getTagRoundnessInput().val(savedStates.tagRoundness);
  2006. getReplaceIconsInMenu().val(savedStates.replaceIconsInMenu);
  2007. getSlimLeftMenuCheckbox().prop('checked', savedStates.slimLeftMenu);
  2008. getHideHomeWidgetsCheckbox().prop('checked', savedStates.hideHomeWidgets);
  2009. getHideDiscoverButtonCheckbox().prop('checked', savedStates.hideDiscoverButton);
  2010. getFixImageGenerationOverlayCheckbox().prop('checked', savedStates.fixImageGenerationOverlay);
  2011. getExtraSpaceBellowLastAnswerCheckbox().prop('checked', savedStates.extraSpaceBellowLastAnswer);
  2012. getModelLabelTextModeSelect().val(savedStates.modelLabelTextMode);
  2013. getModelLabelStyleSelect().val(savedStates.modelLabelStyle);
  2014. getModelLabelOverwriteCyanIconToGrayCheckbox().prop('checked', savedStates.modelLabelOverwriteCyanIconToGray);
  2015. getModelLabelUseIconForReasoningModelsCheckbox().prop('checked', savedStates.modelLabelUseIconForReasoningModels);
  2016. getModelLabelReasoningModelIconGoldCheckbox().prop('checked', savedStates.modelLabelReasoningModelIconGold);
  2017. getCustomModelPopoverCheckbox().prop('checked', savedStates.customModelPopover);
  2018. getTagPaletteCustomTextArea().val((savedStates.tagPaletteCustom || []).join(', '));
  2019. getMainCaptionHtmlTextArea().val(savedStates.mainCaptionHtml);
  2020. }
  2021.  
  2022. function handleSettingsInit() {
  2023. const modalExists = getPerplexityHelperModal().length > 0;
  2024. const firstCheckboxExists = getCoPilotNewThreadAutoSubmitCheckbox().length > 0;
  2025.  
  2026. if (!modalExists || firstCheckboxExists) { return; }
  2027.  
  2028. const $tabButtons = $c(modalTabGroupTabsCls).addClass('flex gap-2 items-end');
  2029.  
  2030. const setActiveTab = (tabName) => {
  2031. $c(modalTabGroupTabsCls).find('> button').each((_, tab) => {
  2032. const $tab = jq(tab);
  2033. if ($tab.attr('data-tab') === tabName) {
  2034. $tab.addClass(modalTabGroupActiveCls);
  2035. } else {
  2036. $tab.removeClass(modalTabGroupActiveCls);
  2037. }
  2038. });
  2039. $c(modalTabGroupContentCls).each((_, tab) => {
  2040. const $tab = jq(tab);
  2041. if ($tab.attr('data-tab') === tabName) {
  2042. $tab.show();
  2043. } else {
  2044. $tab.hide();
  2045. }
  2046. });
  2047. }
  2048.  
  2049. const createTabContent = (tabName, tabLabel) => {
  2050. const $tabButton = jq('<button/>').text(tabLabel).attr('data-tab', tabName).on('click', () => setActiveTab(tabName));
  2051. $tabButtons.append($tabButton);
  2052. const $tabContent = jq('<div/>')
  2053. .addClass(modalTabGroupContentCls)
  2054. .attr('data-tab', tabName);
  2055. getSettingsModalContent().append($tabContent);
  2056. return $tabContent;
  2057. };
  2058.  
  2059. const insertSeparator = () => getSettingsLastTabGroupContent().append('<hr/>');
  2060.  
  2061. // -------------------------------------------------------------------------------------------------------------------
  2062. createTabContent('general', 'General');
  2063.  
  2064. createCheckbox(coPilotNewThreadAutoSubmitCheckboxId, 'Auto Submit New Thread With CoPilot', saveConfigFromForm);
  2065. createCheckbox(coPilotRepeatLastAutoSubmitCheckboxId, 'Auto Submit Repeat With CoPilot', saveConfigFromForm);
  2066. createCheckbox(hideSideMenuCheckboxId, 'Hide Side Menu', saveConfigFromForm);
  2067.  
  2068. createSelect(
  2069. replaceIconsInMenuId,
  2070. 'Replace menu icons',
  2071. Object.values(ICON_REPLACEMENT_MODE).map(value => ({value, label: value})),
  2072. () => {
  2073. saveConfigFromForm();
  2074. replaceIconsInMenu();
  2075. }
  2076. );
  2077.  
  2078. createCheckbox(slimLeftMenuCheckboxId, 'Slim Left Menu', saveConfigFromForm);
  2079. createCheckbox(hideHomeWidgetsCheckboxId, 'Hide Home Page Widgets', saveConfigFromForm);
  2080. createCheckbox(hideDiscoverButtonCheckboxId, 'Hide Discover Button', saveConfigFromForm);
  2081. 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);
  2082. createCheckbox(extraSpaceBellowLastAnswerCheckboxId, 'Add extra space bellow last answer', saveConfigFromForm);
  2083. createSelect(
  2084. modelLabelStyleSelectId,
  2085. 'Model Label Style',
  2086. Object.values(MODEL_LABEL_STYLE).map(value => ({ value, label: value })),
  2087. saveConfigFromForm
  2088. );
  2089. createSelect(
  2090. modelLabelTextModeSelectId,
  2091. 'Model Label Text',
  2092. Object.values(MODEL_LABEL_TEXT_MODE).map(value => ({ value, label: value })),
  2093. saveConfigFromForm
  2094. );
  2095. createCheckbox(modelLabelOverwriteCyanIconToGrayCheckboxId, 'Overwrite Model Icon: Cyan -> Gray', saveConfigFromForm);
  2096. createCheckbox(modelLabelUseIconForReasoningModelsCheckboxId, 'Use icon for reasoning models', saveConfigFromForm);
  2097. createCheckbox(modelLabelReasoningModelIconGoldCheckboxId, 'Use gold color for reasoning model icon', saveConfigFromForm);
  2098. createCheckbox(customModelPopoverCheckboxId, 'Custom Model Popover (Experimental)', saveConfigFromForm);
  2099. createTextArea(mainCaptionHtmlTextAreaId, 'Main Caption HTML', saveConfigFromForm);
  2100. // -------------------------------------------------------------------------------------------------------------------
  2101. createTabContent('tags', 'Tags');
  2102. createTextArea(tagsTextAreaId, 'Tags', saveConfigFromForm, tagsHelpText, [
  2103. {icon: 'l:images', tooltip: 'Lucide Icons', url: 'https://lucide.dev/icons'},
  2104. {icon: 'td:image', tooltip: 'TDesign Icons', url: 'https://tdesign.tencent.com/design/icon-en#header-69'}
  2105. ])
  2106. .prop('rows', 12).css('min-width', '700px').prop('wrap', 'off');
  2107.  
  2108. const paletteLegendContainer = jq('<div/>').attr('id', 'palette-legend-container');
  2109. getSettingsLastTabGroupContent().append(paletteLegendContainer);
  2110. const updatePaletteLegend = () => {
  2111. paletteLegendContainer.empty().append(createPaletteLegend(loadConfig()?.tagPalette));
  2112. };
  2113. updatePaletteLegend();
  2114.  
  2115. createSelect(
  2116. tagPaletteSelectId,
  2117. 'Tag color palette',
  2118. Object.keys(TAGS_PALETTES).map(key => ({value: key, label: key})),
  2119. () => {
  2120. saveConfigFromForm();
  2121. updatePaletteLegend();
  2122. refreshTags();
  2123. }
  2124. );
  2125.  
  2126. createTextArea(
  2127. tagPaletteCustomTextAreaId,
  2128. 'Custom Palette Colors (comma-separated):',
  2129. () => {
  2130. saveConfigFromForm();
  2131. // Update legend and tags only if CUSTOM is the selected palette
  2132. if (getTagPaletteSelect().val() === TAGS_PALETTES.CUSTOM) {
  2133. updatePaletteLegend();
  2134. refreshTags();
  2135. }
  2136. }
  2137. ).prop('rows', 2); // Make it a bit smaller than the main tags text area
  2138.  
  2139. createTagsPreview();
  2140.  
  2141. const FONTS = Object.keys(fontUrls);
  2142.  
  2143. createSelect(
  2144. tagFontSelectId,
  2145. 'Tag font',
  2146. FONTS.map(font => ({ value: font, label: font })),
  2147. () => {
  2148. saveConfigFromForm();
  2149. loadFont(loadConfigOrDefault().tagFont);
  2150. refreshTags({force: true});
  2151. }
  2152. );
  2153. createColorInput(tagColorPickerId, 'Custom color - copy field for tag to clipboard', () => {
  2154. const color = getTagColorPicker().val();
  2155. debugLog('color', color);
  2156. copyTextToClipboard(genColorPart(color));
  2157. });
  2158. const saveConfigFromFormAndForceRefreshTags = () => {
  2159. saveConfigFromForm();
  2160. refreshTags({force: true});
  2161. };
  2162.  
  2163. createCheckbox(tagBoldCheckboxId, 'Bold text', saveConfigFromFormAndForceRefreshTags);
  2164. createCheckbox(tagItalicCheckboxId, 'Italic text', saveConfigFromFormAndForceRefreshTags);
  2165. createNumberInput(
  2166. tagFontSizeInputId,
  2167. 'Font size',
  2168. saveConfigFromFormAndForceRefreshTags,
  2169. { min: 4, max: 64 }
  2170. );
  2171.  
  2172. createNumberInput(
  2173. tagIconSizeInputId,
  2174. 'Icon size',
  2175. saveConfigFromFormAndForceRefreshTags,
  2176. { min: 4, max: 64 }
  2177. );
  2178.  
  2179. createNumberInput(
  2180. tagRoundnessInputId,
  2181. 'Tag Roundness (px)',
  2182. saveConfigFromFormAndForceRefreshTags,
  2183. { min: 0, max: 32 }
  2184. );
  2185.  
  2186. createNumberInput(
  2187. tagTextYOffsetInputId,
  2188. 'Text Y offset',
  2189. saveConfigFromFormAndForceRefreshTags,
  2190. { step: 1, min: -50, max: 50 }
  2191. );
  2192.  
  2193. createNumberInput(
  2194. tagIconYOffsetInputId,
  2195. 'Icon Y offset',
  2196. saveConfigFromFormAndForceRefreshTags,
  2197. { step: 1, min: -50, max: 50 }
  2198. );
  2199.  
  2200. createCheckbox(tagTweakNoBorderCheckboxId, 'No border', saveConfigFromFormAndForceRefreshTags);
  2201. createCheckbox(tagTweakSlimPaddingCheckboxId, 'Slim padding', saveConfigFromFormAndForceRefreshTags);
  2202. createCheckbox(tagTweakRichBorderColorCheckboxId, 'Rich Border Color', saveConfigFromFormAndForceRefreshTags);
  2203. createCheckbox(tagTweakTextShadowCheckboxId, 'Text shadow', saveConfigFromFormAndForceRefreshTags);
  2204. createNumberInput(
  2205. tagLuminanceThresholdInputId,
  2206. 'Tag Luminance Threshold (determines if tag is light or dark)',
  2207. saveConfigFromFormAndForceRefreshTags,
  2208. {step: 0.01, min: 0, max: 1}
  2209. );
  2210. createSelect(
  2211. tagHomePageLayoutSelectId,
  2212. 'Tag container layout on home page (requires page refresh)',
  2213. Object.values(TAG_HOME_PAGE_LAYOUT).map(value => ({value, label: value})),
  2214. saveConfigFromForm
  2215. );
  2216.  
  2217. const $modelsList = jq('<div/>').text('Model IDs: ');
  2218. const modelIds = PP.modelDescriptors.map(md => md.ppModelId).join(', ');
  2219. $modelsList.append(modelIds);
  2220. getSettingsLastTabGroupContent().append($modelsList);
  2221.  
  2222. // -------------------------------------------------------------------------------------------------------------------
  2223. createTabContent('settings', 'Settings');
  2224.  
  2225. 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.'));
  2226.  
  2227. const buttonsContainer = jq('<div/>').addClass('flex gap-2');
  2228. getSettingsLastTabGroupContent().append(buttonsContainer);
  2229.  
  2230. const createExportButton = () => {
  2231. const exportButton = jq('<button>')
  2232. .text('Export Settings')
  2233. .on('click', () => {
  2234. const settings = JSON.stringify(getSavedStates(), null, 2);
  2235. const blob = new Blob([settings], { type: 'application/json' });
  2236. const date = new Date().toISOString().replace(/[:]/g, '-').replace(/T/g, '--').split('.')[0]; // Format: YYYY-MM-DD--HH-MM-SS
  2237. const filename = `perplexity-helper-settings_${date}.json`;
  2238. const url = URL.createObjectURL(blob);
  2239. const a = document.createElement('a');
  2240. a.href = url;
  2241. a.download = filename;
  2242. document.body.appendChild(a);
  2243. a.click();
  2244. document.body.removeChild(a);
  2245. URL.revokeObjectURL(url);
  2246. });
  2247. buttonsContainer.append(exportButton);
  2248. };
  2249.  
  2250. const createImportButton = () => {
  2251. const importButton = jq('<button>')
  2252. .text('Import Settings')
  2253. .on('click', () => {
  2254. const input = jq('<input type="file" accept=".json">');
  2255. input.on('change', async (event) => {
  2256. const file = event.target.files[0];
  2257. if (file) {
  2258. const confirmOverwrite = confirm('This will overwrite your current settings. Do you want to continue?');
  2259. if (confirmOverwrite) {
  2260. const reader = new FileReader();
  2261. reader.onload = (e) => {
  2262. try {
  2263. const importedSettings = JSON.parse(e.target.result);
  2264. saveConfig(importedSettings);
  2265. loadCurrentConfigToSettingsForm();
  2266. alert('Settings imported successfully!');
  2267. } catch (error) {
  2268. console.error('Failed to import settings:', error);
  2269. alert('Failed to import settings. Please ensure the file is valid JSON.');
  2270. }
  2271. };
  2272. reader.readAsText(file);
  2273. }
  2274. }
  2275. });
  2276. input.click();
  2277. });
  2278. buttonsContainer.append(importButton);
  2279. };
  2280.  
  2281. createExportButton();
  2282. createImportButton();
  2283.  
  2284. // -------------------------------------------------------------------------------------------------------------------
  2285. createTabContent('debug', 'Debug'); // debug options at the bottom (do NOT add more normal options bellow this!)
  2286.  
  2287. createCheckbox(enableDebugCheckboxId, 'Enable Debug', saveConfigFromForm);
  2288. createCheckbox(enableTagsDebugCheckboxId, 'Enable tags debug log', saveConfigFromForm);
  2289.  
  2290. // -------------------------------------------------------------------------------------------------------------------
  2291. setActiveTab('general');
  2292. loadCurrentConfigToSettingsForm();
  2293. }
  2294.  
  2295. debugLog(jq.fn.jquery);
  2296. const getSavedStates = () => JSON.parse(localStorage.getItem(storageKey));
  2297.  
  2298. const getModal = () => jq("[data-testid='quick-search-modal'] > div");
  2299. const getCopilotToggleButton = textarea => textarea.parent().parent().find('[data-testid="copilot-toggle"]');
  2300. const upperControls = () => jq('svg[data-icon="lock"] ~ div:contains("Share")').nthParent(5).closest('.flex.justify-between:not(.grid-cols-3)');
  2301.  
  2302. const getControlsArea = () => jq('textarea[placeholder="Ask follow-up"]').parent().parent().children().last();
  2303.  
  2304. const getCopilotNewThreadButton = () => jq('#copilot_new_thread');
  2305. const getCopilotRepeatLastButton = () => jq('#copilot_repeat_last');
  2306. const getSelectAllButton = () => jq('#perplexity_helper_select_all');
  2307. const getSelectAllAndSubmitButton = () => jq('#perplexity_helper_select_all_and_submit');
  2308. const getCopyPlaceholder = () => jq('#perplexity_helper_copy_placeholder');
  2309. const getCopyAndFillInPlaceholder = () => jq('#perplexity_helper_copy_placeholder_and_fill_in');
  2310. const getTopSettingsButtonEl = () => $i(topSettingsButtonId);
  2311. const getLeftSettingsButtonEl = () => $i(leftSettingsButtonId);
  2312. const getSideMenu = () => jq('.min-h-\\[100vh\\]').children().first();
  2313. const getSettingsModalContent = () => getPerplexityHelperModal().find(`.modal-content`);
  2314. const getSettingsLastTabGroupContent = () => getSettingsModalContent().find(`.${modalTabGroupContentCls}`).last();
  2315.  
  2316. const getSubmitBtn0 = () => jq('svg[data-icon="arrow-up"]').last().parent().parent();
  2317. const getSubmitBtn1 = () => jq('svg[data-icon="arrow-right"]').last().parent().parent();
  2318. const getSubmitBtn2 = () => jq('svg[data-icon="code-fork"]').last().parent().parent();
  2319.  
  2320. const isStandardControlsAreaFc = () => !getControlsArea().hasClass('bottom-0');
  2321. const getCurrentControlsArea = () => isStandardControlsAreaFc() ? getControlsArea() : getControlsArea().find('.bottom-0');
  2322.  
  2323. const getDashedCheckboxButton = () => jq('svg[data-icon="square-dashed"]').parent().parent();
  2324. const getStarSVG = () => jq('svg[data-icon="star-christmas"]');
  2325. const getSpecifyQuestionBox = () => jq('svg[data-icon="star-christmas"]').parent().parent().parent().last();
  2326.  
  2327. const getNumberOfDashedSVGs = () => getSpecifyQuestionBox().find('svg[data-icon="square-dashed"]').length;
  2328. const getSpecifyQuestionControlsWrapper = () => getSpecifyQuestionBox().find('button:contains("Continue")').parent()
  2329. const getCopiedModal = () => jq('#copied-modal');
  2330. const getCopiedModal2 = () => jq('#copied-modal-2');
  2331. const getCopyPlaceholderInput = () => getSpecifyQuestionBox().find('textarea');
  2332.  
  2333. const getSubmitButton0or2 = () => getSubmitBtn0().length < 1 ? getSubmitBtn2() : getSubmitBtn0();
  2334.  
  2335. const questionBoxWithPlaceholderExists = () => getSpecifyQuestionBox().find('textarea')?.attr('placeholder')?.length > 0 ?? false;
  2336.  
  2337. // TODO: no longer used? was this for agentic questions?
  2338. const selectAllCheckboxes = () => {
  2339. const currentCheckboxes = getDashedCheckboxButton();
  2340. debugLog('checkboxes', currentCheckboxes);
  2341.  
  2342. const removeLastObject = (arr) => {
  2343. if (!_.isEmpty(arr)) {
  2344. debugLog('arr', arr);
  2345. const newArr = _.dropRight(arr, 1);
  2346. debugLog("newArr", newArr);
  2347. getDashedCheckboxButton().last().click();
  2348.  
  2349. return setTimeout(() => {
  2350. removeLastObject(newArr)
  2351. }, 1)
  2352.  
  2353. }
  2354. };
  2355.  
  2356. removeLastObject(currentCheckboxes);
  2357. }
  2358.  
  2359. const isCopilotOn = (el) => el.hasClass('text-super')
  2360.  
  2361. const toggleBtnDot = (btnDot, value) => {
  2362. debugLog(' toggleBtnDot btnDot', btnDot);
  2363.  
  2364. const btnDotInner = btnDot.find('.rounded-full');
  2365.  
  2366. debugLog('btnDotInner', btnDotInner);
  2367.  
  2368. if (!btnDotInner.hasClass('bg-super') && value === true) {
  2369. btnDot.click();
  2370. }
  2371. }
  2372.  
  2373. const checkForCopilotToggleState = (timer, checkCondition, submitWhenTrue, submitButtonVersion) => {
  2374. debugLog("checkForCopilotToggleState run", timer, checkCondition(), submitWhenTrue, submitButtonVersion);
  2375. if (checkCondition()) {
  2376. clearInterval(timer);
  2377. debugLog("checkForCopilotToggleState condition met, interval cleared");
  2378. const submitBtn = submitButtonVersion === 0 ? getSubmitButton0or2() : getSubmitBtn1();
  2379.  
  2380. debugLog('submitBtn', submitBtn);
  2381. if (submitWhenTrue) {
  2382. submitBtn.click();
  2383. }
  2384. }
  2385. }
  2386.  
  2387. const openNewThreadModal = (lastQuery) => {
  2388. debugLog('openNewThreadModal', lastQuery)
  2389.  
  2390. const newThreadText = jq(".sticky div").filter(function () {
  2391. return /^New Thread$/i.test(jq(this).text());
  2392. });
  2393. if (!newThreadText.length) {
  2394. debugLog('newThreadText.length should be 1', newThreadText.length);
  2395. return;
  2396. }
  2397. debugLog('newThreadText', newThreadText);
  2398.  
  2399. newThreadText.click();
  2400. setTimeout(() => {
  2401. debugLog('newThreadText.click()');
  2402. const modal = getModal();
  2403.  
  2404. if (modal.length > 0) {
  2405. const textArea = modal.find('textarea');
  2406. if (textArea.length !== 1) debugLog('textArea.length should be 1', textArea.length);
  2407.  
  2408. const newTextArea = textArea.last();
  2409. const textareaElement = newTextArea[0];
  2410. debugLog('textareaElement', textareaElement);
  2411. changeValueUsingEvent(textareaElement, lastQuery);
  2412.  
  2413. const copilotButton = getCopilotToggleButton(newTextArea);
  2414.  
  2415. toggleBtnDot(copilotButton, true);
  2416. const isCopilotOnBtn = () => isCopilotOn(copilotButton);
  2417.  
  2418. const coPilotNewThreadAutoSubmit =
  2419. getSavedStates()
  2420. ? getSavedStates().coPilotNewThreadAutoSubmit
  2421. : getCoPilotNewThreadAutoSubmitCheckbox().prop('checked');
  2422.  
  2423. const copilotCheck = () => {
  2424. const ctx = {timer: null};
  2425. ctx.timer = setInterval(() => checkForCopilotToggleState(ctx.timer, isCopilotOnBtn, coPilotNewThreadAutoSubmit, 1), 500);
  2426. }
  2427.  
  2428. copilotCheck();
  2429. } else {
  2430. debugLog('else of modal.length > 0');
  2431. }
  2432. },
  2433. 2000);
  2434. }
  2435.  
  2436. const getLastQuery = () => {
  2437. // wrapper around prompt + response
  2438. const lastQueryBox = jq('svg[data-icon="repeat"]').last().nthParent(7);
  2439. if (lastQueryBox.length === 0) {
  2440. debugLog('lastQueryBox not found');
  2441. }
  2442.  
  2443. const wasCopilotUsed = lastQueryBox.find('svg[data-icon="star-christmas"]').length > 0;
  2444. const lastQueryBoxText = lastQueryBox.find('.whitespace-pre-line').text();
  2445.  
  2446. debugLog('[getLastQuery]', {lastQueryBox, wasCopilotUsed, lastQueryBoxText});
  2447. return lastQueryBoxText ?? null;
  2448. }
  2449.  
  2450. const saveConfigFromForm = () => {
  2451. const checkBoxStates = {
  2452. coPilotNewThreadAutoSubmit: getCoPilotNewThreadAutoSubmitCheckbox().prop('checked'),
  2453. coPilotRepeatLastAutoSubmit: getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked'),
  2454. hideSideMenu: getHideSideMenuCheckbox().prop('checked'),
  2455. tagsText: getTagsTextArea().val(),
  2456. enableDebug: getEnableDebugCheckbox().prop('checked'),
  2457. debugTagsMode: getEnableTagsDebugCheckbox().prop('checked'),
  2458. tagPalette: getTagPaletteSelect().val(),
  2459. tagFont: getTagFontSelect().val(),
  2460. tagTweakNoBorder: getTagTweakNoBorderCheckbox().prop('checked'),
  2461. tagTweakSlimPadding: getTagTweakSlimPaddingCheckbox().prop('checked'),
  2462. tagTweakRichBorderColor: getTagTweakRichBorderColorCheckbox().prop('checked'),
  2463. tagTweakTextShadow: getTagTweakTextShadowCheckbox().prop('checked'),
  2464. tagHomePageLayout: getTagHomePageLayoutSelect().val(),
  2465. tagLuminanceThreshold: parseFloat(getTagLuminanceThresholdInput().val()),
  2466. tagBold: getTagBoldCheckbox().prop('checked'),
  2467. tagItalic: getTagItalicCheckbox().prop('checked'),
  2468. tagFontSize: parseInt(getTagFontSizeInput().val()),
  2469. tagIconSize: parseInt(getTagIconSizeInput().val()),
  2470. tagRoundness: parseInt(getTagRoundnessInput().val()),
  2471. tagTextYOffset: parseInt(getTagTextYOffsetInput().val()),
  2472. tagIconYOffset: parseInt(getTagIconYOffsetInput().val()),
  2473. replaceIconsInMenu: getReplaceIconsInMenu().val(),
  2474. slimLeftMenu: getSlimLeftMenuCheckbox().prop('checked'),
  2475. hideHomeWidgets: getHideHomeWidgetsCheckbox().prop('checked'),
  2476. hideDiscoverButton: getHideDiscoverButtonCheckbox().prop('checked'),
  2477. fixImageGenerationOverlay: getFixImageGenerationOverlayCheckbox().prop('checked'),
  2478. extraSpaceBellowLastAnswer: getExtraSpaceBellowLastAnswerCheckbox().prop('checked'),
  2479. modelLabelTextMode: getModelLabelTextModeSelect().val(),
  2480. modelLabelStyle: getModelLabelStyleSelect().val(),
  2481. modelLabelOverwriteCyanIconToGray: getModelLabelOverwriteCyanIconToGrayCheckbox().prop('checked'),
  2482. modelLabelUseIconForReasoningModels: getModelLabelUseIconForReasoningModelsCheckbox().prop('checked'),
  2483. modelLabelReasoningModelIconGold: getModelLabelReasoningModelIconGoldCheckbox().prop('checked'),
  2484. customModelPopover: getCustomModelPopoverCheckbox().prop('checked'),
  2485. tagPaletteCustom: getTagPaletteCustomTextArea().val().split(',').map(s => s.trim()).filter(Boolean),
  2486. mainCaptionHtml: getMainCaptionHtmlTextArea().val(),
  2487. };
  2488. saveConfig(checkBoxStates);
  2489. };
  2490.  
  2491. const showPerplexityHelperSettingsModal = () => {
  2492. loadCurrentConfigToSettingsForm();
  2493. getPerplexityHelperModal().show().css('display', 'flex');
  2494. }
  2495.  
  2496. const hidePerplexityHelperSettingsModal = () => {
  2497. getPerplexityHelperModal().hide();
  2498. }
  2499.  
  2500. const handleTopSettingsButtonInsertion = () => {
  2501. const copilotHelperSettings = getTopSettingsButtonEl();
  2502. // TODO: no longer works
  2503. // debugLog('upperControls().length > 0', upperControls().length, 'copilotHelperSettings.length', copilotHelperSettings.length, 'upperControls().children().length', upperControls().children().length);
  2504. if (upperControls().length > 0 && copilotHelperSettings.length < 1 && upperControls().children().length >= 1) {
  2505. debugLog('inserting settings button');
  2506. upperControls().children().eq(0).children().eq(0).append(upperButton(topSettingsButtonId, cogIco, 'Perplexity Helper Settings'));
  2507. }
  2508. };
  2509.  
  2510. const handleTopSettingsButtonSetup = () => {
  2511. const settingsButtonEl = getTopSettingsButtonEl();
  2512.  
  2513. if (settingsButtonEl.length === 1 && !settingsButtonEl.attr('data-has-custom-click-event')) {
  2514. debugLog('handleTopSettingsButtonSetup: setting up the button');
  2515. if (settingsButtonEl.length === 0) {
  2516. debugLog('handleTopSettingsButtonSetup: settingsButtonEl.length === 0');
  2517. }
  2518.  
  2519. settingsButtonEl.on("click", () => {
  2520. debugLog('perplexity_helper_settings open click');
  2521. showPerplexityHelperSettingsModal();
  2522. });
  2523.  
  2524. settingsButtonEl.attr('data-has-custom-click-event', true);
  2525. }
  2526. };
  2527.  
  2528. const applySideMenuHiding = () => {
  2529. const sideMenu = getSideMenu();
  2530. if (getSavedStates()) getSavedStates().hideSideMenu || getHideSideMenuCheckbox().prop('checked') ? sideMenu.hide() : sideMenu.show();
  2531. };
  2532.  
  2533. const handleModalCreation = () => {
  2534. if (getPerplexityHelperModal().length > 0) return;
  2535. debugLog('handleModalCreation: creating modal');
  2536. jq("body").append(modalHTML);
  2537.  
  2538. getPerplexityHelperModal().find('.close').on('click', () => {
  2539. debugLog('perplexity_helper_settings close click');
  2540. hidePerplexityHelperSettingsModal();
  2541. });
  2542. };
  2543.  
  2544. const lucideIconMappings = {
  2545. LUCIDE1: leftPanelIconMappingsToLucide1,
  2546. LUCIDE2: leftPanelIconMappingsToLucide2,
  2547. };
  2548.  
  2549. const findKeyByValue = (obj, value) =>
  2550. Object.keys(obj).find(key => obj[key] === value);
  2551.  
  2552. const SUPPORTED_ICON_REPLACEMENT_MODES = [
  2553. ICON_REPLACEMENT_MODE.LUCIDE1,
  2554. ICON_REPLACEMENT_MODE.LUCIDE2,
  2555. ICON_REPLACEMENT_MODE.LUCIDE3,
  2556. ICON_REPLACEMENT_MODE.TDESIGN1,
  2557. ICON_REPLACEMENT_MODE.TDESIGN2,
  2558. ICON_REPLACEMENT_MODE.TDESIGN3,
  2559. ];
  2560.  
  2561. const replaceIconsInMenu = () => {
  2562. const config = loadConfigOrDefault();
  2563. const replacementMode = findKeyByValue(ICON_REPLACEMENT_MODE, config.replaceIconsInMenu);
  2564.  
  2565. if (SUPPORTED_ICON_REPLACEMENT_MODES.includes(config.replaceIconsInMenu)) {
  2566. const processedAttr = `data-${pplxHelperTag}-processed`;
  2567. const iconMapping = iconMappings[replacementMode];
  2568. if (!iconMapping) {
  2569. console.error(logPrefix, '[replaceIconsInMenu] iconMapping not found', { config, iconMappings });
  2570. return;
  2571. }
  2572.  
  2573. const svgEls = PP.getIconsInLeftPanel().find('svg[data-icon]');
  2574. // debugLog('[replaceIconsInMenu] svgEls', svgEls);
  2575. svgEls.each((_, svgEl) => {
  2576. const $svg = jq(svgEl);
  2577. const processed = $svg.attr(processedAttr);
  2578. if (processed) return;
  2579.  
  2580. const iconName = $svg.attr('data-icon');
  2581. debugLog('[replaceIconsInMenu] iconName', iconName);
  2582. const replacementIconName = iconMapping[iconName];
  2583. debugLog('[replaceIconsInMenu] replacementIconName', replacementIconName);
  2584.  
  2585. $svg.attr(processedAttr, true);
  2586. if (replacementIconName) {
  2587. const isTDesign = config.replaceIconsInMenu.startsWith('TDesign');
  2588. const newIconUrl = (isTDesign ? getTDesignIconUrl : getLucideIconUrl)(replacementIconName);
  2589.  
  2590. debugLog('[replaceIconsInMenu] replacing icon', { iconName, replacementIconName, svgEl, newIconUrl });
  2591. $svg.hide();
  2592. const newIconEl = jq('<img>').attr('src', newIconUrl).addClass('invert opacity-50');
  2593. if (isTDesign) newIconEl.addClass('h-6');
  2594. $svg.parent().addClass(lucideIconParentCls);
  2595. $svg.after(newIconEl);
  2596. } else {
  2597. if (!['plus', 'thread'].includes(iconName)) {
  2598. console.error('[replaceIconsInMenu] no replacement icon found', { iconName, replacementIconName });
  2599. }
  2600. }
  2601. });
  2602. }
  2603. };
  2604.  
  2605. const handleLeftSettingsButtonSetup = () => {
  2606. const existingLeftSettingsButton = getLeftSettingsButtonEl();
  2607. if (existingLeftSettingsButton.length === 1) {
  2608. const wrapper = existingLeftSettingsButton.parent();
  2609. if (!wrapper.is(':last-child')) {
  2610. wrapper.appendTo(wrapper.parent());
  2611. }
  2612. return;
  2613. }
  2614.  
  2615. const leftPanel = PP.getIconsInLeftPanel();
  2616.  
  2617. if (leftPanel.length === 0) {
  2618. debugLog('handleLeftSettingsButtonSetup: leftPanel not found');
  2619. }
  2620.  
  2621. const wrapperEl = jq('<div>').attr('id', leftSettingsButtonWrapperId);
  2622. const iconEl = jq(cogIco);
  2623.  
  2624. const btnEl = jq('<button>')
  2625. .attr('id', leftSettingsButtonId)
  2626. .attr('title', 'Perplexity Helper Settings')
  2627. .addClass('text-textOff dark:text-textOffDark font-sans text-xs flex items-center gap-x-sm')
  2628. .addClass('md:hover:bg-offsetPlus text-textOff dark:text-textOffDark md:hover:text-textMain dark:md:hover:bg-offsetPlusDark dark:md:hover:text-textMainDark font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-in-out font-sans select-none items-center relative group justify-center text-center items-center rounded-full cursor-point active:scale-95 origin-center whitespace-nowrap inline-flex text-sm px-sm font-medium h-8')
  2629. .append(iconEl)
  2630. .append(jq('<span>').text('Perplexity Helper'))
  2631. ;
  2632. btnEl.on('click', () => {
  2633. debugLog('left settings button clicked');
  2634. showPerplexityHelperSettingsModal();
  2635. });
  2636. wrapperEl.append(btnEl);
  2637. leftPanel.append(wrapperEl);
  2638. }
  2639.  
  2640. const handleSlimLeftMenu = () => {
  2641. const config = loadConfigOrDefault();
  2642. if (!config.slimLeftMenu) return;
  2643.  
  2644. const leftPanel = PP.getLeftPanel();
  2645. if (leftPanel.length === 0) {
  2646. debugLog('handleSlimLeftMenu: leftPanel not found');
  2647. }
  2648. if (leftPanel.length !== 1) {
  2649. console.warn(logPrefix, '[handleSlimLeftMenu] too many leftPanel found', leftPanel);
  2650. }
  2651.  
  2652. leftPanel.addClass(leftPanelSlimCls);
  2653. };
  2654.  
  2655. const handleHideHomeWidgets = () => {
  2656. const config = loadConfigOrDefault();
  2657. if (!config.hideHomeWidgets) return;
  2658.  
  2659. const homeWidgets = PP.getHomeWidgets();
  2660. if (homeWidgets.length === 0) {
  2661. debugLog('handleHideHomeWidgets: homeWidgets not found');
  2662. return;
  2663. }
  2664. if (homeWidgets.length > 1) {
  2665. console.warn(logPrefix, '[handleHideHomeWidgets] too many homeWidgets found', homeWidgets);
  2666. }
  2667.  
  2668. homeWidgets.hide();
  2669. };
  2670.  
  2671. const handleFixImageGenerationOverlay = () => {
  2672. const config = loadConfigOrDefault();
  2673. if (!config.fixImageGenerationOverlay) return;
  2674.  
  2675. const imageGenerationOverlay = PP.getImageGenerationOverlay();
  2676. if (imageGenerationOverlay.length === 0) {
  2677. // debugLog('handleFixImageGenerationOverlay: imageGenerationOverlay not found');
  2678. return;
  2679. }
  2680.  
  2681. // only if wrench button is cyan (we are in custom prompt)
  2682. if (!imageGenerationOverlay.find('button').hasClass('bg-super')) return;
  2683.  
  2684. const transform = imageGenerationOverlay.css('transform');
  2685. if (!transform) return;
  2686. // Handle both matrix and translate formats
  2687. const matrixMatch = transform.match(/matrix\(.*,\s*([\d.]+),\s*([\d.]+)\)/);
  2688. const translateMatch = transform.match(/translate\(([\d.]+)px(?:,\s*([\d.]+)px)?\)/);
  2689. const currentX = matrixMatch
  2690. ? matrixMatch[1] // Matrix format: 5th value is X translation
  2691. : translateMatch?.[1] || 0; // Translate format: first value
  2692. debugLog('[handleFixImageGenerationOverlay] currentX', currentX, 'transform', transform);
  2693. imageGenerationOverlay.css({
  2694. transform: `translate(${currentX}px, 0px)`
  2695. });
  2696. };
  2697.  
  2698. const handleExtraSpaceBellowLastAnswer = () => {
  2699. const config = loadConfigOrDefault();
  2700. if (!config.extraSpaceBellowLastAnswer) return;
  2701. jq('body')
  2702. .find(`.erp-tab\\:rounded-none > .max-w-screen > .scrollable-container > .mx-auto > .mx-auto > .relative > div > div.bottom-mobileNavHeight`)
  2703. .prev()
  2704. .css({
  2705. // backgroundColor: 'magenta',
  2706. paddingBottom: '15em'
  2707. })
  2708. ;
  2709. };
  2710.  
  2711. const handleSearchPage = () => {
  2712. const controlsArea = getCurrentControlsArea();
  2713. controlsArea.addClass(controlsAreaCls);
  2714. controlsArea.parent().find('textarea').first().addClass(textAreaCls);
  2715. controlsArea.addClass(roundedMD);
  2716. controlsArea.parent().addClass(roundedMD);
  2717.  
  2718.  
  2719. if (controlsArea.length === 0) {
  2720. debugLog('controlsArea not found', {
  2721. controlsArea,
  2722. currentControlsArea: getCurrentControlsArea(),
  2723. isStandardControlsAreaFc: isStandardControlsAreaFc()
  2724. });
  2725. }
  2726.  
  2727. const lastQueryBoxText = getLastQuery();
  2728.  
  2729. const mainTextArea = isStandardControlsAreaFc() ? controlsArea.prev().prev() : controlsArea.parent().prev();
  2730.  
  2731. if (mainTextArea.length === 0) {
  2732. debugLog('mainTextArea not found', mainTextArea);
  2733. }
  2734.  
  2735.  
  2736. debugLog('lastQueryBoxText', { lastQueryBoxText });
  2737. if (lastQueryBoxText) {
  2738. const copilotNewThread = getCopilotNewThreadButton();
  2739. const copilotRepeatLast = getCopilotRepeatLastButton();
  2740.  
  2741. if (controlsArea.length > 0 && copilotNewThread.length < 1) {
  2742. controlsArea.append(button('copilot_new_thread', robotIco, "Starts new thread for with last query text and Copilot ON", standardButtonCls));
  2743. }
  2744.  
  2745. // Due to updates in Perplexity, this is unnecessary for now
  2746. // if (controlsArea.length > 0 && copilotRepeatLast.length < 1) {
  2747. // controlsArea.append(button('copilot_repeat_last', robotRepeatIco, "Repeats last query with Copilot ON"));
  2748. // }
  2749.  
  2750. if (!copilotNewThread.attr('data-has-custom-click-event')) {
  2751. copilotNewThread.on("click", function () {
  2752. debugLog('copilotNewThread Button clicked!');
  2753. openNewThreadModal(getLastQuery());
  2754. });
  2755. copilotNewThread.attr('data-has-custom-click-event', true);
  2756. }
  2757.  
  2758. if (!copilotRepeatLast.attr('data-has-custom-click-event')) {
  2759. copilotRepeatLast.on("click", function () {
  2760. const controlsArea = getCurrentControlsArea();
  2761. const textAreaElement = controlsArea.parent().find('textarea')[0];
  2762.  
  2763. const coPilotRepeatLastAutoSubmit =
  2764. getSavedStates()
  2765. ? getSavedStates().coPilotRepeatLastAutoSubmit
  2766. : getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked');
  2767.  
  2768. debugLog('coPilotRepeatLastAutoSubmit', coPilotRepeatLastAutoSubmit);
  2769. changeValueUsingEvent(textAreaElement, getLastQuery());
  2770. const copilotToggleButton = getCopilotToggleButton(mainTextArea);
  2771. debugLog('mainTextArea', mainTextArea);
  2772. debugLog('copilotToggleButton', copilotToggleButton);
  2773.  
  2774. toggleBtnDot(copilotToggleButton, true);
  2775. const isCopilotOnBtn = () => isCopilotOn(copilotToggleButton);
  2776.  
  2777. const copilotCheck = () => {
  2778. const ctx = { timer: null };
  2779. ctx.timer = setInterval(() => checkForCopilotToggleState(ctx.timer, isCopilotOnBtn, coPilotRepeatLastAutoSubmit, 0), 500);
  2780. };
  2781.  
  2782. copilotCheck();
  2783. debugLog('copilot_repeat_last Button clicked!');
  2784. });
  2785. copilotRepeatLast.attr('data-has-custom-click-event', true);
  2786. }
  2787. }
  2788.  
  2789. if (getNumberOfDashedSVGs() > 0 && getNumberOfDashedSVGs() === getDashedCheckboxButton().length
  2790. && getSelectAllButton().length < 1 && getSelectAllAndSubmitButton().length < 1) {
  2791. debugLog('getNumberOfDashedSVGs() === getNumberOfDashedSVGs()', getNumberOfDashedSVGs());
  2792. debugLog('getSpecifyQuestionBox', getSpecifyQuestionBox());
  2793.  
  2794. const specifyQuestionControlsWrapper = getSpecifyQuestionControlsWrapper();
  2795. debugLog('specifyQuestionControlsWrapper', specifyQuestionControlsWrapper);
  2796. const selectAllButton = textButton('perplexity_helper_select_all', 'Select all', 'Selects all options');
  2797. const selectAllAndSubmitButton = textButton('perplexity_helper_select_all_and_submit', 'Select all & submit', 'Selects all options and submits');
  2798.  
  2799. specifyQuestionControlsWrapper.append(selectAllButton);
  2800. specifyQuestionControlsWrapper.append(selectAllAndSubmitButton);
  2801.  
  2802. getSelectAllButton().on("click", function () {
  2803. selectAllCheckboxes();
  2804. });
  2805.  
  2806. getSelectAllAndSubmitButton().on("click", function () {
  2807. selectAllCheckboxes();
  2808. setTimeout(() => {
  2809. getSpecifyQuestionControlsWrapper().find('button:contains("Continue")').click();
  2810. }, 200);
  2811. });
  2812. }
  2813.  
  2814. const constructClipBoard = (buttonId, buttonGetter, modalGetter, copiedModalId, elementGetter) => {
  2815. const placeholderValue = getSpecifyQuestionBox().find('textarea').attr('placeholder');
  2816.  
  2817. const clipboardInstance = new ClipboardJS(`#${buttonId}`, {
  2818. text: () => placeholderValue
  2819. });
  2820.  
  2821. const copiedModal = `<span id="${copiedModalId}">Copied!</span>`;
  2822. debugLog('copiedModalId', copiedModalId);
  2823. debugLog('copiedModal', copiedModal);
  2824.  
  2825. jq('main').append(copiedModal);
  2826.  
  2827. clipboardInstance.on('success', _ => {
  2828. var buttonPosition = buttonGetter().position();
  2829. jq(`#${copiedModalId}`).css({
  2830. top: buttonPosition.top - 30,
  2831. left: buttonPosition.left + 50
  2832. }).show();
  2833.  
  2834. if (elementGetter !== undefined) {
  2835. changeValueUsingEvent(elementGetter()[0], placeholderValue);
  2836. }
  2837.  
  2838. setTimeout(() => {
  2839. modalGetter().hide();
  2840. }, 5000);
  2841. });
  2842. };
  2843.  
  2844. if (questionBoxWithPlaceholderExists() && getCopyPlaceholder().length < 1) {
  2845. const copyPlaceholder = textButton('perplexity_helper_copy_placeholder', 'Copy placeholder', 'Copies placeholder value');
  2846. const copyPlaceholderAndFillIn = textButton('perplexity_helper_copy_placeholder_and_fill_in', 'Copy placeholder and fill in',
  2847. 'Copies placeholder value and fills in input');
  2848.  
  2849. const specifyQuestionControlsWrapper = getSpecifyQuestionControlsWrapper();
  2850.  
  2851. specifyQuestionControlsWrapper.append(copyPlaceholder);
  2852. specifyQuestionControlsWrapper.append(copyPlaceholderAndFillIn);
  2853.  
  2854. constructClipBoard('perplexity_helper_copy_placeholder', getCopyPlaceholder, getCopiedModal, 'copied-modal');
  2855. constructClipBoard('perplexity_helper_copy_placeholder_and_fill_in', getCopyAndFillInPlaceholder, getCopiedModal2, 'copied-modal-2', getCopyPlaceholderInput);
  2856. }
  2857. }
  2858.  
  2859. const getLabelFromModelDescription = modelLabelStyle => modelLabelFromAriaLabel => modelDescription => {
  2860. if (!modelDescription) return modelLabelFromAriaLabel;
  2861. switch (modelLabelStyle) {
  2862. case MODEL_LABEL_TEXT_MODE.FULL_NAME:
  2863. return modelDescription.nameEn;
  2864. case MODEL_LABEL_TEXT_MODE.SHORT_NAME:
  2865. return modelDescription.nameEnShort ?? modelDescription.nameEn;
  2866. case MODEL_LABEL_TEXT_MODE.PP_MODEL_ID:
  2867. return modelDescription.ppModelId;
  2868. case MODEL_LABEL_TEXT_MODE.OWN_NAME_VERSION_SHORT:
  2869. const nameText = modelDescription.ownNameEn ?? modelDescription.nameEn;
  2870. const versionText = modelDescription.ownVersionEnShort ?? modelDescription.ownVersionEn;
  2871. return [nameText, versionText].filter(Boolean).join(modelDescription.ownNameVersionSeparator ?? ' ');
  2872. default:
  2873. throw new Error(`Unknown model label style: ${modelLabelStyle}`);
  2874. }
  2875. }
  2876.  
  2877. const getExtraClassesFromModelLabelStyle = modelLabelStyle => {
  2878. switch (modelLabelStyle) {
  2879. case MODEL_LABEL_STYLE.BUTTON_SUBTLE:
  2880. return modelLabelStyleButtonSubtleCls;
  2881. case MODEL_LABEL_STYLE.BUTTON_WHITE:
  2882. return modelLabelStyleButtonWhiteCls;
  2883. case MODEL_LABEL_STYLE.BUTTON_CYAN:
  2884. return modelLabelStyleButtonCyanCls;
  2885. default:
  2886. return '';
  2887. }
  2888. }
  2889. const handleModelLabel = () => {
  2890. const config = loadConfigOrDefault();
  2891. if (!config.modelLabelStyle || config.modelLabelStyle === MODEL_LABEL_STYLE.OFF) return;
  2892.  
  2893. const $modelIcons = PP.getAnyModelButton();
  2894. $modelIcons.each((_, el) => {
  2895. const $el = jq(el);
  2896. if (!$el.find(`.${modelLabelCls}`).length) {
  2897. $el.prepend(jq(`<span class="${modelLabelCls}"></span>`));
  2898. }
  2899. if (!$el.hasClass(modelIconButtonCls)) {
  2900. $el.addClass(modelIconButtonCls);
  2901. }
  2902. const $label = $el.find(`.${modelLabelCls}`);
  2903. const modelDescription = PP.getModelDescriptionFromModelButton($el);
  2904. const modelLabelFromAriaLabel = $el.attr('aria-label');
  2905. const modelLabel = getLabelFromModelDescription(config.modelLabelTextMode)(modelLabelFromAriaLabel)(modelDescription);
  2906. if (!modelLabel) {
  2907. console.error('[handleModelLabel] modelLabel is null', { modelDescription, modelLabelFromAriaLabel, $el });
  2908. return;
  2909. }
  2910. const extraClasses = [
  2911. getExtraClassesFromModelLabelStyle(config.modelLabelStyle),
  2912. config.modelLabelOverwriteCyanIconToGray ? modelLabelOverwriteCyanIconToGrayCls : '',
  2913. ].filter(Boolean).join(' ');
  2914. $label.attr('data-extra-classes', extraClasses);
  2915. if ($label.text() !== modelLabel) {
  2916. $label.attr('data-model-description', JSON.stringify(modelDescription));
  2917. $label.text(modelLabel);
  2918. $label.addClass(extraClasses);
  2919. }
  2920. if (config.modelLabelUseIconForReasoningModels) {
  2921. if ($el.find(`.${reasoningModelCls}`).length === 0) {
  2922. const iconUrl = getLucideIconUrl('lightbulb');
  2923. const $icon = jq(`<img src="${iconUrl}" alt="Reasoning model" class="${reasoningModelCls}" />`);
  2924. if (config.modelLabelReasoningModelIconGold) {
  2925. $icon.addClass(iconColorGoldCls);
  2926. }
  2927. $el.prepend($icon);
  2928. }
  2929. const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`);
  2930. const isReasoning = modelDescription?.modelType === 'reasoning';
  2931. if (isReasoning) {
  2932. $reasoningModelIcon.css({display: 'inline-block'});
  2933. $label.css({padding: '3px 8px 0 0'});
  2934. if (config.modelLabelStyle === MODEL_LABEL_STYLE.JUST_TEXT) {
  2935. $reasoningModelIcon.addClass(iconColorGrayCls);
  2936. } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_CYAN) {
  2937. $reasoningModelIcon.addClass(iconColorCyanCls);
  2938. } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_SUBTLE) {
  2939. $reasoningModelIcon.addClass(iconColorGrayCls);
  2940. } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_WHITE) {
  2941. $reasoningModelIcon.addClass(iconColorWhiteCls);
  2942. }
  2943. } else {
  2944. $reasoningModelIcon.remove();
  2945. $label.css({padding: '3px 6px 0 10px'});
  2946. }
  2947. }
  2948. });
  2949. }
  2950.  
  2951. const handleHideDiscoverButton = () => {
  2952. const config = loadConfigOrDefault();
  2953. if (!config.hideDiscoverButton) return;
  2954. const $iconsInLeftPanel = PP.getIconsInLeftPanel().find('div.space-y-xs.flex-1:has(a[href="/discover"])').children().eq(1);
  2955. $iconsInLeftPanel.hide();
  2956. }
  2957.  
  2958. const handleCustomModelPopover = () => {
  2959. const config = loadConfigOrDefault();
  2960. if (!config.customModelPopover) return;
  2961.  
  2962. const $modelSelectionList = PP.getModelSelectionList();
  2963. if ($modelSelectionList.length === 0) return;
  2964. const processedAttr = 'ph-processed-custom-model-popover';
  2965. if ($modelSelectionList.attr(processedAttr)) return;
  2966. $modelSelectionList.attr(processedAttr, true);
  2967. $modelSelectionList.nthParent(2).css({maxHeight: 'initial'});
  2968. $modelSelectionList.css({
  2969. display: 'grid',
  2970. gridTemplateColumns: '1fr 1fr',
  2971. gap: '10px',
  2972. 'grid-auto-rows': 'min-content',
  2973. });
  2974.  
  2975. // Place items in first column
  2976. $modelSelectionList.children(":nth-child(3)").css("grid-area", "1 / 1");
  2977. $modelSelectionList.children(":nth-child(4)").css("grid-area", "2 / 1");
  2978. $modelSelectionList.children(":nth-child(5)").css("grid-area", "3 / 1");
  2979. $modelSelectionList.children(":nth-child(6)").css("grid-area", "4 / 1");
  2980.  
  2981. // Place items in second column
  2982. $modelSelectionList.children(":nth-child(8)").css("grid-area", "1 / 2");
  2983. $modelSelectionList.children(":nth-child(9)").css("grid-area", "2 / 2");
  2984. $modelSelectionList.children(":nth-child(10)").css("grid-area", "3 / 2");
  2985. $modelSelectionList.children(":nth-child(11)").css("grid-area", "4 / 2");
  2986. // $modelSelectionList.children(":nth-child(2)").css("grid-area", "5 / 2");
  2987. $modelSelectionList.children(":nth-child(1)").css("grid-area", "5 / 2").css("border-top", "1px solid #343637");
  2988.  
  2989. $modelSelectionList.children(":nth-child(2)").css("display", "none");
  2990. }
  2991.  
  2992. const mainCaptionAppliedCls = genCssName('mainCaptionApplied');
  2993. const handleMainCaptionHtml = () => {
  2994. const config = loadConfigOrDefault();
  2995. if (!config.mainCaptionHtml) return;
  2996. if (PP.getMainCaption().hasClass(mainCaptionAppliedCls)) return;
  2997. PP.setMainCaptionHtml(config.mainCaptionHtml);
  2998. PP.getMainCaption().addClass(mainCaptionAppliedCls);
  2999. }
  3000.  
  3001. const work = () => {
  3002. handleModalCreation();
  3003. handleTopSettingsButtonInsertion();
  3004. handleTopSettingsButtonSetup();
  3005. handleSettingsInit();
  3006. handleLeftSettingsButtonSetup();
  3007. handleSlimLeftMenu();
  3008. handleHideHomeWidgets();
  3009. handleExtraSpaceBellowLastAnswer();
  3010. applySideMenuHiding();
  3011. replaceIconsInMenu();
  3012. handleModelLabel();
  3013. handleHideDiscoverButton();
  3014. handleCustomModelPopover();
  3015. handleMainCaptionHtml();
  3016. const regex = /^https:\/\/www\.perplexity\.ai\/search\/?.*/;
  3017. const currentUrl = jq(location).attr('href');
  3018. const matchedCurrentUrlAsSearchPage = regex.test(currentUrl);
  3019.  
  3020. // debugLog("currentUrl", currentUrl);
  3021. // debugLog("matchedCurrentUrlAsSearchPage", matchedCurrentUrlAsSearchPage);
  3022.  
  3023. if (matchedCurrentUrlAsSearchPage) {
  3024. handleSearchPage();
  3025. }
  3026. };
  3027.  
  3028. const fontUrls = {
  3029. Roboto: 'https://fonts.cdnfonts.com/css/roboto',
  3030. Montserrat: 'https://fonts.cdnfonts.com/css/montserrat',
  3031. Lato: 'https://fonts.cdnfonts.com/css/lato',
  3032. Oswald: 'https://fonts.cdnfonts.com/css/oswald-4',
  3033. Raleway: 'https://fonts.cdnfonts.com/css/raleway-5',
  3034. 'Ubuntu Mono': 'https://fonts.cdnfonts.com/css/ubuntu-mono',
  3035. Nunito: 'https://fonts.cdnfonts.com/css/nunito',
  3036. Poppins: 'https://fonts.cdnfonts.com/css/poppins',
  3037. 'Playfair Display': 'https://fonts.cdnfonts.com/css/playfair-display',
  3038. Merriweather: 'https://fonts.cdnfonts.com/css/merriweather',
  3039. 'Fira Sans': 'https://fonts.cdnfonts.com/css/fira-sans',
  3040. Quicksand: 'https://fonts.cdnfonts.com/css/quicksand',
  3041. Comfortaa: 'https://fonts.cdnfonts.com/css/comfortaa-3',
  3042. 'Almendra': 'https://fonts.cdnfonts.com/css/almendra',
  3043. 'Enchanted Land': 'https://fonts.cdnfonts.com/css/enchanted-land',
  3044. 'Cinzel Decorative': 'https://fonts.cdnfonts.com/css/cinzel-decorative',
  3045. 'Orbitron': 'https://fonts.cdnfonts.com/css/orbitron',
  3046. 'Exo 2': 'https://fonts.cdnfonts.com/css/exo-2',
  3047. 'Chakra Petch': 'https://fonts.cdnfonts.com/css/chakra-petch',
  3048. 'Open Sans Condensed': 'https://fonts.cdnfonts.com/css/open-sans-condensed',
  3049. 'Saira Condensed': 'https://fonts.cdnfonts.com/css/saira-condensed',
  3050. Inter: 'https://cdn.jsdelivr.net/npm/@fontsource/inter@4.5.0/index.min.css',
  3051. };
  3052.  
  3053. const loadFont = (fontName) => {
  3054. const fontUrl = fontUrls[fontName];
  3055. debugLog('loadFont', { fontName, fontUrl });
  3056. if (fontUrl) {
  3057. const link = document.createElement('link');
  3058. link.rel = 'stylesheet';
  3059. link.href = fontUrl;
  3060. document.head.appendChild(link);
  3061. }
  3062. };
  3063.  
  3064. const setupFixImageGenerationOverlay = () => {
  3065. const config = loadConfigOrDefault();
  3066. if (config.fixImageGenerationOverlay) {
  3067. setInterval(handleFixImageGenerationOverlay, 250);
  3068. }
  3069. };
  3070.  
  3071. (function () {
  3072. if (loadConfigOrDefault()?.enableDebug) {
  3073. enableDebugMode();
  3074. }
  3075.  
  3076. debugLog('TAGS_PALETTES', TAGS_PALETTES);
  3077. if (loadConfigOrDefault()?.debugTagsMode) {
  3078. enableTagsDebugging();
  3079. }
  3080.  
  3081. 'use strict';
  3082. jq("head").append(`<style>${styles}</style>`);
  3083.  
  3084. setupTags();
  3085. setupFixImageGenerationOverlay();
  3086.  
  3087. const mainInterval = setInterval(work, 1000);
  3088. window.ph = {
  3089. stopWork: () => { clearInterval(mainInterval); },
  3090. work,
  3091. jq,
  3092. showPerplexityHelperSettingsModal,
  3093. enableTagsDebugging: () => { debugTags = true; },
  3094. disableTagsDebugging: () => { debugTags = false; },
  3095. }
  3096.  
  3097. loadFont(loadConfigOrDefault().tagFont);
  3098.  
  3099. console.log(`%c${userscriptName}%c\n %cTiartyos%c & %cmonnef%c\n ... loaded`,
  3100. 'color: #aaffaa; font-size: 1.5rem; background-color: rgba(0, 0, 0, 0.5); padding: 2px;',
  3101. '',
  3102. 'color: #6b02ff; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;',
  3103. '',
  3104. 'color: #aa2cc3; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;',
  3105. '',
  3106. '');
  3107. }());

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址