Perplexity helper

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

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

QingJ © 2025

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