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

QingJ © 2025

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