Perplexity helper

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

// ==UserScript==
// @name        Perplexity helper
// @namespace   Tiartyos
// @match       https://www.perplexity.ai/*
// @grant       none
// @version     5.0
// @author      Tiartyos, monnef
// @description Simple script that adds buttons to Perplexity website for repeating request using Copilot.
// @require     https://code.jquery.com/jquery-3.6.0.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/lodash-fp/0.10.4/lodash-fp.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/index.unpkg.umd.js
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/showdown.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/jsondiffpatch/0.4.1/jsondiffpatch.umd.min.js
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/lib/perplex-plus.js
// @homepageURL https://www.perplexity.ai/
// @license     GPL-3.0-or-later
// ==/UserScript==

const PP = window.PP.noConflict();
const jq = PP.jq;
const $c = (cls, parent) => jq(`.${cls}`, parent);
const $i = (id, parent) => jq(`#${id}`, parent);
const takeStr = n => str => str.slice(0, n);
const dropStr = n => str => str.slice(n);
const filter = pred => xs => xs.filter(pred);
const nl = '\n';
const markdownConverter = new showdown.Converter({ tables: true });

let debugMode = false;
const enableDebugMode = () => {
  debugMode = true;
};

const userscriptName = 'Perplexity helper';
const logPrefix = `[${userscriptName}]`;

const debugLog = (...args) => {
  if (debugMode) {
    console.debug(logPrefix, ...args);
  }
}

let debugTags = false;
const debugLogTags = (...args) => {
  if (debugTags) {
    console.debug(logPrefix, '[tags]', ...args);
  }
}

const enableTagsDebugging = () => {
  debugTags = true;
}

($ => {
  $.fn.nthParent = function (n) {
    let $p = $(this);
    if (!(n > -0)) { return $() }
    let p = 1 + n;
    while (p--) { $p = $p.parent(); }
    return $p;
  };
})(jq);

// unpkg had quite often problems, tens of seconds to load, sometime 503 fails
// const getLucideIconUrl = iconName => `https://unpkg.com/lucide-static@latest/icons/${iconName}.svg`;
const getLucideIconUrl = (iconName) => `https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/${iconName}.svg`;

const getTDesignIconUrl = iconName => `https://api.iconify.design/tdesign:${iconName}.svg`;

const parseIconName = iconName => {
  if (!iconName.includes(':')) return { typePrefix: 'l', processedIconName: iconName };
  const [typePrefix, processedIconName] = iconName.split(':');
  return { typePrefix, processedIconName };
};

const getIconUrl = iconName => {
  const {typePrefix, processedIconName} = parseIconName(iconName);
  if (typePrefix === 'td') {
    return getTDesignIconUrl(processedIconName);
  }
  if (typePrefix === 'l') {
    return getLucideIconUrl(processedIconName);
  }
  throw new Error(`Unknown icon type: ${typePrefix}`);
}

const pplxHelperTag = 'pplx-helper';
const genCssName = x => `${pplxHelperTag}--${x}`;

const button = (id, icoName, title, extraClass) => `<button title="${title}" type="button" id="${id}" class="btn-helper bg-super dark:bg-superDark dark:text-backgroundDark text-white hover:opacity-80 font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-in-out font-sans  select-none items-center relative group  justify-center text-center items-center rounded-full cursor-point active:scale-95 origin-center whitespace-nowrap inline-flex text-base aspect-square h-10 ${extraClass}"  >
<div class="flex items-center leading-none justify-center gap-xs">
    ${icoName}
</div></button>`;

const upperButton = (id, icoName, title) => `
<div title="${title}" id="${id}" class="border rounded-full px-sm py-xs flex items-center gap-x-sm  border-borderMain/60 dark:border-borderMainDark/60 divide-borderMain dark:divide-borderMainDark ring-borderMain dark:ring-borderMainDark  bg-transparent cursor-pointer"><div class="border-borderMain/60 dark:border-borderMainDark/60 divide-borderMain dark:divide-borderMainDark ring-borderMain dark:ring-borderMainDark  bg-transparent"><div class="flex items-center gap-x-xs transition duration-300 select-none hover:text-superAlt light font-sans text-sm font-medium text-textOff dark:text-textOffDark selection:bg-super selection:text-white dark:selection:bg-opacity-50 selection:bg-opacity-70"><div class="">${icoName}<path fill="currentColor" d="M64 288L39.8 263.8C14.3 238.3 0 203.8 0 167.8C0 92.8 60.8 32 135.8 32c36 0 70.5 14.3 96 39.8L256 96l24.2-24.2c25.5-25.5 60-39.8 96-39.8C451.2 32 512 92.8 512 167.8c0 36-14.3 70.5-39.8 96L448 288 256 480 64 288z"></path></svg></div><div></div></div></div></div>
`

const textButton = (id, text, title) => `
<button title="${title}" id="${id}" type="button" class="bg-super text-white hover:opacity-80 font-sans focus:outline-none outline-none transition duration-300 ease-in-out font-sans  select-none items-center relative group justify-center rounded-md cursor-point active:scale-95 origin-center whitespace-nowrap inline-flex text-sm px-sm font-medium h-8">
<div class="flex items-center leading-none justify-center gap-xs"><span class="flex items-center relative ">${text}</span></div></button>
`
const icoColor = '#1F1F1F';
const robotIco = `<svg style="width: 23px; fill: ${icoColor};" viewBox="0 0 640 512" xmlns="http://www.w3.org/2000/svg"><path d="m32 224h32v192h-32a31.96166 31.96166 0 0 1 -32-32v-128a31.96166 31.96166 0 0 1 32-32zm512-48v272a64.06328 64.06328 0 0 1 -64 64h-320a64.06328 64.06328 0 0 1 -64-64v-272a79.974 79.974 0 0 1 80-80h112v-64a32 32 0 0 1 64 0v64h112a79.974 79.974 0 0 1 80 80zm-280 80a40 40 0 1 0 -40 40 39.997 39.997 0 0 0 40-40zm-8 128h-64v32h64zm96 0h-64v32h64zm104-128a40 40 0 1 0 -40 40 39.997 39.997 0 0 0 40-40zm-8 128h-64v32h64zm192-128v128a31.96166 31.96166 0 0 1 -32 32h-32v-192h32a31.96166 31.96166 0 0 1 32 32z"/></svg>`;
const robotRepeatIco = `<svg style="width: 23px; fill: ${icoColor};"  viewBox="0 0 640 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/">    <path d="M442.179,325.051L442.179,459.979C442.151,488.506 418.685,511.972 390.158,512L130.053,512C101.525,511.972 78.06,488.506 78.032,459.979L78.032,238.868C78.032,203.208 107.376,173.863 143.037,173.863L234.095,173.863L234.095,121.842C234.095,107.573 245.836,95.832 260.105,95.832C274.374,95.832 286.116,107.573 286.116,121.842L286.116,173.863L309.247,173.863C321.515,245.71 373.724,304.005 442.179,325.051ZM26.011,277.905L52.021,277.905L52.021,433.968L25.979,433.968C11.727,433.968 -0,422.241 -0,407.989L-0,303.885C-0,289.633 11.727,277.905 25.979,277.905L26.011,277.905ZM468.19,331.092C478.118,332.676 488.289,333.497 498.65,333.497C505.935,333.497 513.126,333.091 520.211,332.299L520.211,407.989C520.211,422.241 508.483,433.968 494.231,433.968L468.19,433.968L468.19,331.092ZM208.084,407.958L156.063,407.958L156.063,433.968L208.084,433.968L208.084,407.958ZM286.116,407.958L234.095,407.958L234.095,433.968L286.116,433.968L286.116,407.958ZM364.147,407.958L312.126,407.958L312.126,433.968L364.147,433.968L364.147,407.958ZM214.587,303.916C214.587,286.08 199.91,271.403 182.074,271.403C164.238,271.403 149.561,286.08 149.561,303.916C149.561,321.752 164.238,336.429 182.074,336.429C182.075,336.429 182.075,336.429 182.076,336.429C199.911,336.429 214.587,321.753 214.587,303.918C214.587,303.917 214.587,303.917 214.587,303.916ZM370.65,303.916C370.65,286.08 355.973,271.403 338.137,271.403C320.301,271.403 305.624,286.08 305.624,303.916C305.624,321.752 320.301,336.429 338.137,336.429C338.138,336.429 338.139,336.429 338.139,336.429C355.974,336.429 370.65,321.753 370.65,303.918C370.65,303.917 370.65,303.917 370.65,303.916Z" style="fill-rule:nonzero;"/>
    <g transform="matrix(14.135,0,0,14.135,329.029,-28.2701)">
        <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM17.19,15.94C17.15,16.03 17.1,16.11 17.03,16.18L15.34,17.87C15.19,18.02 15,18.09 14.81,18.09C14.62,18.09 14.43,18.02 14.28,17.87C13.99,17.58 13.99,17.1 14.28,16.81L14.69,16.4L9.1,16.4C7.8,16.4 6.75,15.34 6.75,14.05L6.75,12.28C6.75,11.87 7.09,11.53 7.5,11.53C7.91,11.53 8.25,11.87 8.25,12.28L8.25,14.05C8.25,14.52 8.63,14.9 9.1,14.9L14.69,14.9L14.28,14.49C13.99,14.2 13.99,13.72 14.28,13.43C14.57,13.14 15.05,13.14 15.34,13.43L17.03,15.12C17.1,15.19 17.15,15.27 17.19,15.36C17.27,15.55 17.27,15.76 17.19,15.94ZM17.25,11.72C17.25,12.13 16.91,12.47 16.5,12.47C16.09,12.47 15.75,12.13 15.75,11.72L15.75,9.95C15.75,9.48 15.37,9.1 14.9,9.1L9.31,9.1L9.72,9.5C10.01,9.79 10.01,10.27 9.72,10.56C9.57,10.71 9.38,10.78 9.19,10.78C9,10.78 8.81,10.71 8.66,10.56L6.97,8.87C6.9,8.8 6.85,8.72 6.81,8.63C6.73,8.45 6.73,8.24 6.81,8.06C6.85,7.97 6.9,7.88 6.97,7.81L8.66,6.12C8.95,5.83 9.43,5.83 9.72,6.12C10.01,6.41 10.01,6.89 9.72,7.18L9.31,7.59L14.9,7.59C16.2,7.59 17.25,8.65 17.25,9.94L17.25,11.72Z" style="fill-rule:nonzero;"/>
    </g></svg>`;

const cogIco = `<svg style="width: 23px; fill: rgb(141, 145, 145);" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"viewBox="0 0 38.297 38.297"
\t xml:space="preserve">
<g>
\t<path d="M25.311,18.136l2.039-2.041l-2.492-2.492l-2.039,2.041c-1.355-0.98-2.941-1.654-4.664-1.934v-2.882H14.63v2.883
\t\tc-1.722,0.278-3.308,0.953-4.662,1.934l-2.041-2.041l-2.492,2.492l2.041,2.041c-0.98,1.354-1.656,2.941-1.937,4.662H2.658v3.523
\t\tH5.54c0.279,1.723,0.955,3.309,1.937,4.664l-2.041,2.039l2.492,2.492l2.041-2.039c1.354,0.979,2.94,1.653,4.662,1.936v2.883h3.524
\t\tv-2.883c1.723-0.279,3.309-0.955,4.664-1.936l2.039,2.039l2.492-2.492l-2.039-2.039c0.98-1.355,1.654-2.941,1.934-4.664h2.885
\t\tv-3.524h-2.885C26.967,21.078,26.293,19.492,25.311,18.136z M16.393,30.869c-3.479,0-6.309-2.83-6.309-6.307
\t\tc0-3.479,2.83-6.308,6.309-6.308c3.479,0,6.307,2.828,6.307,6.308C22.699,28.039,19.871,30.869,16.393,30.869z M35.639,8.113v-2.35
\t\th-0.965c-0.16-0.809-0.474-1.561-0.918-2.221l0.682-0.683l-1.664-1.66l-0.68,0.683c-0.658-0.445-1.41-0.76-2.217-0.918V0h-2.351
\t\tv0.965c-0.81,0.158-1.562,0.473-2.219,0.918L24.625,1.2l-1.662,1.66l0.683,0.683c-0.445,0.66-0.761,1.412-0.918,2.221h-0.966v2.35
\t\th0.966c0.157,0.807,0.473,1.559,0.918,2.217l-0.681,0.68l1.658,1.664l0.685-0.682c0.657,0.443,1.409,0.758,2.219,0.916v0.967h2.351
\t\tv-0.968c0.807-0.158,1.559-0.473,2.217-0.916l0.682,0.68l1.662-1.66l-0.682-0.682c0.444-0.658,0.758-1.41,0.918-2.217H35.639
\t\tL35.639,8.113z M28.701,10.677c-2.062,0-3.74-1.678-3.74-3.74c0-2.064,1.679-3.742,3.74-3.742c2.064,0,3.742,1.678,3.742,3.742
\t\tC32.443,9,30.766,10.677,28.701,10.677z"/>
</g>
</svg>`;


const perplexityHelperModalId = 'perplexityHelperModal';
const getPerplexityHelperModal = () => $i(perplexityHelperModalId);

const modalSettingsTitleCls = genCssName('modal-settings-title');

const gitlabLogo = classes => `
<svg class="${classes}" fill="#000000" width="800px" height="800px" viewBox="0 0 512 512" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><path d="M494.07,281.6l-25.18-78.08a11,11,0,0,0-.61-2.1L417.78,44.48a20.08,20.08,0,0,0-19.17-13.82A19.77,19.77,0,0,0,379.66,44.6L331.52,194.15h-152L131.34,44.59a19.76,19.76,0,0,0-18.86-13.94h-.11a20.15,20.15,0,0,0-19.12,14L42.7,201.73c0,.14-.11.26-.16.4L16.91,281.61a29.15,29.15,0,0,0,10.44,32.46L248.79,476.48a11.25,11.25,0,0,0,13.38-.07L483.65,314.07a29.13,29.13,0,0,0,10.42-32.47m-331-64.51L224.8,408.85,76.63,217.09m209.64,191.8,59.19-183.84,2.55-8h86.52L300.47,390.44M398.8,59.31l43.37,134.83H355.35M324.16,217l-43,133.58L255.5,430.14,186.94,217M112.27,59.31l43.46,134.83H69M40.68,295.58a6.19,6.19,0,0,1-2.21-6.9l19-59L197.08,410.27M470.34,295.58,313.92,410.22l.52-.69L453.5,229.64l19,59a6.2,6.2,0,0,1-2.19,6.92"/></svg>
`;

const modalLargeIconAnchorClasses = 'hover:scale-110 opacity-50 hover:opacity-100 transition-all duration-300';

const modalTabGroupTabsCls = genCssName('modal-tab-group-tabs');
const modalTabGroupActiveCls = genCssName('modal-tab-group-active');
const modalTabGroupContentCls = genCssName('modal-tab-group-content');
const modalTabGroupSeparatorCls = genCssName('modal-tab-group-separator');

const modalHTML = `
<div id="${perplexityHelperModalId}" class="modal">
  <div class="modal-content">
    <span class="close">&times;</span>
    <h1 class="flex items-center gap-4">
      <span class="mr-4 ${modalSettingsTitleCls}">Perplexity Helper</span>
      <a href="https://gitlab.com/Tiartyos/perplexity-helper"
        target="_blank" title="GitLab Repository"
        class="${modalLargeIconAnchorClasses}"
      >
        ${gitlabLogo('w-8 h-8 invert')}
      </a>
      <a href="https://tiartyos.gitlab.io/perplexity-helper/"
        target="_blank" title="Web Page"
        class="${modalLargeIconAnchorClasses}"
      >
        <img src="${getLucideIconUrl('globe')}" class="w-8 h-8 invert">
      </a>
    </h1>
    <p class="text-xs opacity-30 mt-1 mb-3">Changes may require page refresh.</p>
    <div class="${modalTabGroupTabsCls}">
    </div>
    <hr class="!mt-0 !mb-0 ${modalTabGroupSeparatorCls}">
  </div>
</div>
`;

const tagsContainerCls = genCssName('tags-container');
const tagContainerCompactCls = genCssName('tag-container-compact');
const tagContainerWiderCls = genCssName('tag-container-wider');
const tagContainerWideCls = genCssName('tag-container-wide');
const tagContainerExtraWideCls = genCssName('tag-container-extra-wide');
const threadTagContainerCls = genCssName('thread-tag-container');
const newTagContainerCls = genCssName('new-tag-container');
const newTagContainerInCollectionCls = genCssName('new-tag-container-in-collection');
const tagCls = genCssName('tag');
const tagDarkTextCls = genCssName('tag-dark-text');
const tagIconCls = genCssName('tag-icon');
const tagPaletteCls = genCssName('tag-palette');
const tagPaletteItemCls = genCssName('tag-palette-item');
const tagTweakNoBorderCls = genCssName('tag-tweak-no-border');
const tagTweakSlimPaddingCls = genCssName('tag-tweak-slim-padding');
const tagsPreviewCls = genCssName('tags-preview');
const tagsPreviewNewCls = genCssName('tags-preview-new');
const tagsPreviewThreadCls = genCssName('tags-preview-thread');
const tagsPreviewNewInCollectionCls = genCssName('tags-preview-new-in-collection');
const tagTweakTextShadowCls = genCssName('tag-tweak-text-shadow');
const tagFenceCls = genCssName('tag-fence');
const tagAllFencesWrapperCls = genCssName('tag-all-fences-wrapper');
const tagRestOfTagsWrapperCls = genCssName('tag-rest-of-tags-wrapper');
const tagFenceContentCls = genCssName('tag-fence-content');
const tagDirectoryCls = genCssName('tag-directory');
const tagDirectoryContentCls = genCssName('tag-directory-content');
const helpTextCls = genCssName('help-text');
const queryBoxCls = genCssName('query-box');
const controlsAreaCls = genCssName('controls-area');
const textAreaCls = genCssName('text-area');
const standardButtonCls = genCssName('standard-button');
const lucideIconParentCls = genCssName('lucide-icon-parent');
const roundedMD = genCssName('rounded-md');
const leftPanelSlimCls = genCssName('left-panel-slim');
const modelLabelCls = genCssName('model-label');
const modelLabelStyleJustTextCls = genCssName('model-label-style-just-text');
const modelLabelStyleButtonSubtleCls = genCssName('model-label-style-button-subtle');
const modelLabelStyleButtonWhiteCls = genCssName('model-label-style-button-white');
const modelLabelStyleButtonCyanCls = genCssName('model-label-style-button-cyan');
const modelLabelOverwriteCyanIconToGrayCls = genCssName('model-label-overwrite-cyan-icon-to-gray');
const reasoningModelCls = genCssName('reasoning-model');
const iconColorCyanCls = genCssName('icon-color-cyan');
const iconColorGrayCls = genCssName('icon-color-gray');
const iconColorWhiteCls = genCssName('icon-color-white');

const topSettingsButtonId = genCssName('settings-button-top');
const leftSettingsButtonId = genCssName('settings-button-left');
const leftSettingsButtonWrapperId = genCssName('settings-button-left-wrapper');

const cyanPerplexityColor = '#1fb8cd';
const cyanMediumPerplexityColor = '#204b51';
const cyanDarkPerplexityColor = '#203133';

const styles = `
.textarea_wrapper {
  display: flex;
  flex-direction: column;
}

@import url('https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600&display=swap');

.textarea_wrapper > textarea {
  width: 100%;
  background-color: rgba(0, 0, 0, 0.8);
  padding: 0 5px;
  border-radius: 0.5em;
}

.textarea_label {
}

.${helpTextCls} {
  background-color: #225;
  padding: 0.3em 0.7em;
  border-radius: 0.5em;
  margin: 1em 0;
}
.${helpTextCls} {
  cursor: text;
}

.${helpTextCls} a {
  text-decoration: underline;
}
.${helpTextCls} a:hover {
  color: white;
}

.${helpTextCls} code {
  font-size: 80%;
  background-color: rgba(255, 255, 255, 0.1);
  border-radius: 0.3em;
  padding: 0.1em;
}
.${helpTextCls} pre > code {
  background: none;
}
.${helpTextCls} pre {
  font-size: 80%;
  overflow: auto;
  background-color: rgba(255, 255, 255, 0.1);
  border-radius: 0.3em;
  padding: 0.1em 1em;
}
.${helpTextCls} li {
  list-style: circle;
  margin-left: 1em;
}
.${helpTextCls} hr {
  margin: 1em 0 0.5em 0;
  border-color: rgba(255, 255, 255, 0.1);
}

.${helpTextCls} table {
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0.5em;
  display: inline-block;
}
.${helpTextCls} table td, .${helpTextCls} table th {
  padding: 0.1em 0.5em;
}

.btn-helper {
margin-left: 20px
}

.modal {
  display: none;
  position: fixed;
  z-index: 1000;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: auto;
  background-color: rgba(0, 0, 0, 0.8)
}

.modal-content {
  display: flex;
  margin: 1em auto;
  width: calc(100vw - 2em);
  padding: 20px;
  border: 1px solid #333;
  background-color: #202025;
  border-radius: 6px;
  color: rgb(206, 206, 210);
  flex-direction: column;
  position: relative;
  overflow-y: auto;
  cursor: default;
  font-family: 'Fira Sans', sans-serif;
}

.${modalTabGroupTabsCls} {
  display: flex;
  flex-direction: row;
}

.modal-content .${modalTabGroupTabsCls} > button {
  border-radius: 0.5em 0.5em 0 0;
  border-bottom: 0;
  padding: 0.2em 0.5em 0 0.5em;
  background-color: #1e293b;
  color: rgba(255, 255, 255, 0.5);
  outline-bottom: none;
}

.modal-content .${modalTabGroupTabsCls} > button.${modalTabGroupActiveCls} {
  /* background-color: #3b82f6; */
  color: white;
  text-shadow: 0 0 1px currentColor;
  padding: 0.3em 0.5em 0.2em 0.5em;
}

.modal-content .${modalTabGroupContentCls} {
  display: flex;
  flex-direction: column;
  gap: 1em;
  padding-top: 1em;
}

.${modalSettingsTitleCls} {
  background: linear-gradient(to bottom, white, gray);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  font-weight: bold;
  font-size: 3em;
  text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
  user-select: none;
  margin-top: -0.33em;
  margin-bottom: -0.33em;
}

.modal-content .hover\\:scale-110:hover {
  transform: scale(1.1);
}

.modal-content label {
  padding-right: 10px;
}

.modal-content hr {
  height: 1px;
  margin: 1em 0;
  border-color: rgba(255, 255, 255, 0.1);
}

.modal-content hr.${modalTabGroupSeparatorCls} {
  margin: 0 -1em 0 -1em;
}

.modal-content input[type="checkbox"] {
  appearance: none;
  width: 1.2em;
  height: 1.2em;
  border: 2px solid #ffffff80;
  border-radius: 0.25em;
  background-color: transparent;
  transition: all 0.2s ease;
  cursor: pointer;
  position: relative;
}

.modal-content input[type="checkbox"]:checked {
  background-color: #3b82f6;
  border-color: #3b82f6;
}

.modal-content input[type="checkbox"]:checked::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0.4em;
  height: 0.7em;
  border: solid white;
  border-width: 0 2px 2px 0;
  transform: translate(-50%, -60%) rotate(45deg);
}

.modal-content input[type="checkbox"]:hover {
  border-color: #ffffff;
}

.modal-content input[type="checkbox"]:focus {
  outline: 2px solid #3b82f680;
  outline-offset: 2px;
}

.modal-content .checkbox_label {
  color: white;
  line-height: 1.5;
}

.modal-content .checkbox_wrapper {
  display: flex;
  align-items: center;
  gap: 0.5em;
}

.modal-content .number_label {
  margin-left: 0.5em;
}

.modal-content .color_wrapper {
  display: flex;
  align-items: center;
}

.modal-content .color_label {
  margin-left: 0.5em;
}

.modal-content input, .modal-content button {
  background-color: #1e293b;
  border: 2px solid #ffffff80;
  border-radius: 0.5em;
  color: white;
  padding: 0.5em;
  transition: border-color 0.3s ease, outline 0.3s ease;
}

.modal-content input:hover, .modal-content button:hover {
  border-color: #ffffff;
}

.modal-content input:focus, .modal-content button:focus {
  outline: 2px solid #3b82f680;
  outline-offset: 2px;
}

.modal-content input[type="number"] {
  padding: 0.5em;
  transition: border-color 0.3s ease, outline 0.3s ease;
}

.modal-content input[type="color"] {
  padding: 0;
  height: 2em;
}

.modal-content input[type="color"]:hover {
  border-color: #ffffff;
}

.modal-content input[type="color"]:focus {
  outline: 2px solid #3b82f680;
  outline-offset: 2px;
}

.modal-content h1 + hr {
  margin-top: 0.5em;
}


.modal-content select {
  appearance: none;
  background-color: #1e293b; /* Dark blue background */
  border: 2px solid #ffffff80;
  border-radius: 0.5em;
  padding: 0.3em 2em 0.3em 0.5em;
  color: white;
  font-size: 1em;
  cursor: pointer;
  transition: all 0.2s ease;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: right 0.5em center;
  background-size: 1.2em;
}

.modal-content select option {
  background-color: #1e293b; /* Match select background */
  color: white;
  padding: 0.5em;
}

.modal-content select:hover {
  border-color: #ffffff;
}

.modal-content select:focus {
  outline: 2px solid #3b82f680;
  outline-offset: 2px;
}

.modal-content .select_label {
  color: white;
  margin-left: 0.5em;
}

.modal-content .select_wrapper {
  display: flex;
  align-items: center;
}

.close {
  color: rgb(206, 206, 210);
  float: right;
  font-size: 28px;
  font-weight: bold;
  position: absolute;
  right: 20px;
  top: 5px;
}

.close:hover,
.close:focus {
  color: white;
  text-decoration: none;
  cursor: pointer;
}

#copied-modal,#copied-modal-2 {
  padding: 5px 5px;
  background:gray;
  position:absolute;
  display: none;
  color: white;
  font-size: 15px;
}

label > div.select-none {
  user-select: text;
  cursor: initial;
}

.${tagsContainerCls} {
  display: flex;
  flex-direction: row;
  margin: 5px 0;
}
.${tagsContainerCls}.${threadTagContainerCls} {
  margin-left: 0.5em;
  margin-right: 0.5em;
  margin-bottom: 2px;
}

.${tagContainerCompactCls} {
  margin-top: -2em;
  margin-bottom: 1px;
}
.${tagContainerCompactCls} .${tagFenceCls} {
  margin: 0;
  padding: 1px;
}
.${tagContainerCompactCls} .${tagCls} {
}
.${tagContainerCompactCls} .${tagAllFencesWrapperCls} {
  gap: 1px;
}
.${tagContainerCompactCls} .${tagRestOfTagsWrapperCls} {
  margin: 1px;
}
.${tagContainerCompactCls} .${tagRestOfTagsWrapperCls},
.${tagContainerCompactCls} .${tagFenceContentCls},
.${tagContainerCompactCls} .${tagDirectoryContentCls} {
  gap: 1px;
}

.${tagContainerWiderCls} {
  margin-left: -6em;
  margin-right: -6em;
  margin-bottom: 2em;
}
.${tagContainerWiderCls} .${tagCls} {
}

.${tagContainerWideCls} {
  margin-left: -12em;
  margin-right: -12em;
  margin-bottom: 3em;
}

.${tagContainerExtraWideCls} {
  margin-left: -16em;
  margin-right: -16em;
  margin-bottom: 3em;
}

.${tagCls} {
  border: 1px solid #3b3b3b;
  background-color: #282828;
  /*color: rgba(255, 255, 255, 0.482);*/ /* equivalent of #909090; when on #282828 background */
  padding: 0px 8px 0 8px;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.2s, color 0.2s;
  display: inline-block;
  color: #E8E8E6;
  user-select: none;
}
.${tagCls}.${tagDarkTextCls} {
  color: #171719;
}
.${tagCls} span {
  display: inline-block;
}

.${tagCls}.${tagTweakNoBorderCls} {
  border: none;
}

.${tagCls}.${tagTweakSlimPaddingCls} {
  padding: 0px 4px 0 4px;
}

.${tagCls} .${tagIconCls} {
  width: 16px;
  height: 16px;
  margin-right: 2px;
  margin-left: -4px;
  margin-top: -4px;
  vertical-align: middle;
  display: inline-block;
  filter: invert(1);
}
.${tagCls}.${tagDarkTextCls} .${tagIconCls} {
  filter: none;
}
.${tagCls}.${tagTweakSlimPaddingCls} .${tagIconCls} {
  margin-left: -2px;
}
.${tagCls} span {
  position: relative;
  top: 1.5px;
}
.${tagCls}.${tagTweakTextShadowCls} span {
  text-shadow: 1px 0 0.5px black, -1px 0 0.5px black, 0 1px 0.5px black, 0 -1px 0.5px black;
}
.${tagCls}.${tagTweakTextShadowCls}.${tagDarkTextCls} span {
  text-shadow: 1px 0 0.5px white, -1px 0 0.5px white, 0 1px 0.5px white, 0 -1px 0.5px white;
}
.${tagCls}:hover {
  background-color: #333;
  color: #fff;
  transform: scale(1.02);
}
.${tagCls}.${tagDarkTextCls}:hover {
  /* color: #171717; */
  color: #2f2f2f;
}
.${tagCls}:active {
  transform: scale(0.98);
}

.${tagPaletteCls} {
  display: flex;
  flex-wrap: wrap;
  gap: 1px;
}
.${tagPaletteCls} .${tagPaletteItemCls} {
  text-shadow: 1px 0 1px black, -1px 0 1px black, 0 1px 1px black, 0 -1px 1px black;
  width: 40px;
  height: 25px;
  display: inline-block;
  text-align: center;
  padding: 0 2px;
  transition: color 0.2s, border 0.1s;
  border: 2px solid transparent;
}

.${tagPaletteItemCls}:hover {
  cursor: pointer;
  color: white;
  border: 2px solid white;
}

.${tagsPreviewCls} {
  background-color: #191a1a;
  padding: 0.5em 1em;
  border-radius: 1em;
}

.${tagAllFencesWrapperCls} {
  display: flex;
  flex-direction: row;
  gap: 5px;
}

.${tagRestOfTagsWrapperCls} {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 5px;
  margin: 8px;
}

.${tagFenceCls} {
  display: flex;
  margin: 5px 0;
  padding: 5px;
  border-radius: 4px;
}

.${tagFenceContentCls} {
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  gap: 5px;
}

.${tagDirectoryCls} {
  position: relative;
  display: flex;
  z-index: 100;
}
.${tagDirectoryCls}:hover .${tagDirectoryContentCls} {
  display: flex;
}
.${tagDirectoryContentCls} {
  position: absolute;
  display: none;
  flex-direction: column;
  gap: 5px;
  top: 0px;
  padding-bottom: 1px;
  left: -5px;
  transform: translateY(-100%);
  background: rgba(0, 0, 0, 0.5);
  padding: 5px;
  border-radius: 4px;
  flex-wrap: nowrap;
  width: max-content;
}
.${tagDirectoryContentCls} .${tagCls} {
  white-space: nowrap;
  width: fit-content;
}

.${queryBoxCls} {
  flex-wrap: wrap;
}

.${controlsAreaCls} {
  grid-template-columns: repeat(4,minmax(0,1fr))
}

.${textAreaCls} {
  grid-column-end: 5;
}

.${standardButtonCls} {
  grid-column-start: 4;
}

.${roundedMD} {
  border-radius: 0.375rem!important;
}

#${leftSettingsButtonId} svg {
  transition: fill 0.2s;
}
#${leftSettingsButtonId}:hover svg {
  fill: #fff !important;
}

.w-collapsedSideBarWidth #${leftSettingsButtonId} span {
  display: none;
}

.w-collapsedSideBarWidth #${leftSettingsButtonId} {
  width: 100%;
  border-radius: 0.25rem;
  height: 40px;
}

#${leftSettingsButtonWrapperId} {
  display: flex;
  padding: 0.1em 0.2em;
  justify-content: flex-start;
}

.w-collapsedSideBarWidth #${leftSettingsButtonWrapperId} {
  justify-content: center;
}

.${lucideIconParentCls} > img {
  transition: opacity 0.2s ease;
}

.${lucideIconParentCls}:hover > img, a.dark\\:text-textMainDark .${lucideIconParentCls} > img {
  opacity: 1;
}

.${leftPanelSlimCls}.w-collapsedSideBarWidth,
.${leftPanelSlimCls} .w-collapsedSideBarWidth {
  width: 50px;
}

/* active marker */
.${leftPanelSlimCls} .w-collapsedSideBarWidth .absolute.rounded-l-sm.right-0 {
    /* transform: translateX(-3px) !important; */
    right: 3px;
}

.${modelLabelCls} {
  color: #888;
  /* padding is from style attr */
  transition: color 0.2s, background-color 0.2s, border 0.2s;
}
button:hover > .${modelLabelCls} {
  color: #fff;
}
button:has(> .${modelLabelCls}) {
  padding-right: 0.75em;
}

button:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}) {
  border: 1px solid #333;
}
button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}) {
  background: #333 !important;
}

.${modelLabelCls}.${modelLabelStyleButtonWhiteCls} {
  color: #fff;
}
button:hover > .${modelLabelCls}.${modelLabelStyleButtonWhiteCls} {
  color: #8D9191 !important;
}
button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}) {
  background: #2D2F2F !important;
}
button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}) {
  color: #8D9191 !important;
}

.${modelLabelCls}.${modelLabelStyleButtonCyanCls} {
  color: ${cyanPerplexityColor};
}
button:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) {
  border: 1px solid ${cyanMediumPerplexityColor};
  background: ${cyanDarkPerplexityColor} !important;
}
button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) {
  border: 1px solid ${cyanPerplexityColor};
}

button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}) {
  color: #888 !important;
}

.${reasoningModelCls} {
  width: 16px;
  height: 16px;
  margin-right: 6px;
  margin-left: 10px;
  margin-top: -2px;
  filter: invert();
}
button:has(.${reasoningModelCls}) > div > div > svg {
  width: 32px;
  height: 16px;
  margin-left: 8px;
  margin-right: 12px;
  margin-top: -2px;
  min-width: 16px;
}
button:has(.${reasoningModelCls}) > div > div:has(svg) {
  width: 16px;
  height: 16px;
  min-width: 30px;
}


.${iconColorCyanCls} {
  filter: invert(54%) sepia(84%) saturate(431%) hue-rotate(139deg) brightness(97%) contrast(90%);
  transition: filter 0.2s;
}

.${iconColorGrayCls} {
  filter: invert(50%);
  transition: filter 0.2s;
}
button:has(.${reasoningModelCls}):hover .${iconColorGrayCls} {
  filter: invert(100%);
}

.${iconColorWhiteCls} {
  filter: invert(100%);
  transition: filter 0.2s;
}
button:has(.${reasoningModelCls}):hover .${iconColorWhiteCls} {
  filter: invert(50%);
}
`;

const TAG_POSITION = {
  BEFORE: 'before',
  AFTER: 'after',
  CARET: 'caret',
};

const TAG_CONTAINER_TYPE = {
  NEW: 'new',
  NEW_IN_COLLECTION: 'new-in-collection',
  THREAD: 'thread',
  ALL: 'all',
}

const tagsHelpText = `
Each line is one tag.
Non-field text is what will be inserted into prompt.
Field is denoted by \`<\` and \`>\`, field name is before \`:\`, field value after \`:\`.

Supported fields:
- \`label\`: tag label shown on tag "box" (new items around prompt input area)
- \`position\`: where the tag text will be inserted, default is \`before\`; valid values are \`before\`/\`after\` (existing text) or \`caret\` (at cursor position)
- \`color\`: tag color; CSS colors supported, you can use colors from a pre-generated palette via \`%\` syntax, e.g. \`<color:%5>\`. See palette bellow.
- \`tooltip\`: shown on hover (aka title); (default) tooltip can be disabled when this field is set to empty string - \`<tooltip:>\`
- \`target\`: where the tag will be inserted, default is \`new\`; valid values are \`new\` (on home page or when clicking on "New Thread" button) / \`thread\` (on thread page) / \`all\` (everywhere)
- \`hide\`: hide the tag from the tag list
- \`link\`: link to a URL, e.g. \`<link:https://example.com>\`, can be used for collections. only one link per tag is supported.
- \`link-target\`: target of the link, e.g. \`<link-target:_blank>\` (opens in new tab), default is \`_self\` (same tab).
- \`icon\`: Lucide icon name, e.g. \`<icon:arrow-right>\`. see [lucide icons](https://lucide.dev/icons). prefix \`td:\` is used for [TDesign icons](https://tdesign.tencent.com/design/icon-en#header-69). prefix \`l:\` for Lucide icons is implicit and can be omitted.
- \`set-mode\`: set the query mode: \`pro\` or \`deep-research\`, e.g. \`<set-mode:pro>\`
- \`set-model\`: set the model, e.g. \`<set-model:claude-3-7-sonnet-thinking>\`
- \`set-sources\`: set the sources, e.g. \`<set-sources:001>\` for disabled first source (web), disabled second source (academic), enabled third source (social)
- \`auto-submit\`: automatically submit the query after the tag is clicked (applies after other tag actions like \`set-mode\` or \`set-model\`), e.g. \`<auto-submit>\`
- \`dir\`: unique identifier for a directory tag (it will not insert text into prompt)
- \`in-dir\`: identifier of the parent directory this tag belongs to
- \`fence\`: unique identifier for a fence definition (hidden by default)
- \`in-fence\`: identifier of the fence this tag belongs to
- \`fence-width\`: CSS width for a fence, e.g. \`<fence-width:10em>\`
- \`fence-border-style\`: CSS border style for a fence (e.g., solid, dashed, dotted)
- \`fence-border-color\`: CSS color or a palette \`%\` syntax for a fence border
- \`fence-border-width\`: CSS width for a fence border

---

| String | Replacement | Example |
|---|---|---|
| \`\\n\` | newline | |
| \`$$time$$\` | current time | \`23:05\` |

---

Examples:
\`\`\`
stable diffusion web ui - <label:SDWU>
, prefer concise modern syntax and style, <position:caret><label:concise modern>
tell me a joke<label:Joke><tooltip:>
tell me a joke<label:Joke & Submit><auto-submit>
\`\`\`

Directory example:
\`\`\`
<dir:games>Games<icon:gamepad-2>
<in-dir:games>FFXIV: <color:%15><label:FFXIV>
<in-dir:games>Vintage Story - <label:VS>
\`\`\`

Fence example:
\`\`\`
<fence:anime><fence-border-style:dashed><fence-border-color:%10>
<in-fence:anime>Shounen
<in-fence:anime>Seinen
<in-fence:anime>Shoujo
\`\`\`

Another fence example:
\`\`\`
<fence:programming><fence-border-style:solid><fence-border-color:%20>
<in-fence:programming>Haskell
<in-fence:programming>Raku<label:🦋>
\`\`\`
`.trim();

const defaultTagColor = '#282828';

const changeValueUsingEvent = (selector, value) => {
  debugLog('changeValueUsingEvent', value, selector);

  const nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
  nativeTextareaValueSetter.call(selector, value);
  const inputEvent = new Event('input', {bubbles: true});
  selector.dispatchEvent(inputEvent);
}

const TAGS_PALETTE_COLORS_NUM = 16;
const TAGS_PALETTE_CLASSIC = Object.freeze((() => {
  const step = 360 / TAGS_PALETTE_COLORS_NUM;
  const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  return _.flow(
    _.map(x => startH + x * step, _),
    _.map(h => color2k.hsla(h, startS, startL, startA), _),
    _.sortBy(x => color2k.parseToHsla(x)[0], _)
  )(_.range(0, TAGS_PALETTE_COLORS_NUM));
})());

const TAGS_PALETTE_PASTEL = Object.freeze((() => {
  const step = 360 / TAGS_PALETTE_COLORS_NUM;
  const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  return _.flow(
    _.map(x => startH + x * step, _),
    _.map(h => color2k.hsla(h, startS - 0.2, startL + 0.2, startA), _),
    _.sortBy(x => color2k.parseToHsla(x)[0], _)
  )(_.range(0, TAGS_PALETTE_COLORS_NUM));
})());

const TAGS_PALETTE_GRIM = Object.freeze((() => {
  const step = 360 / TAGS_PALETTE_COLORS_NUM;
  const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  return _.flow(
    _.map(x => startH + x * step, _),
    _.map(h => color2k.hsla(h, startS - 0.6, startL - 0.3, startA), _),
    _.sortBy(x => color2k.parseToHsla(x)[0], _)
  )(_.range(0, TAGS_PALETTE_COLORS_NUM));
})());

const TAGS_PALETTE_DARK = Object.freeze((() => {
  const step = 360 / TAGS_PALETTE_COLORS_NUM;
  const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  return _.flow(
    _.map(x => startH + x * step, _),
    _.map(h => color2k.hsla(h, startS, startL - 0.4, startA), _),
    _.sortBy(x => color2k.parseToHsla(x)[0], _)
  )(_.range(0, TAGS_PALETTE_COLORS_NUM));
})());

const TAGS_PALETTE_GRAY = Object.freeze((() => {
  const step = 1 / TAGS_PALETTE_COLORS_NUM;
  return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(0, 0, step * x, 1));
})());

const TAGS_PALETTE_CYAN = Object.freeze((() => {
  const step = 1 / TAGS_PALETTE_COLORS_NUM;
  const [startH, startS, startL, startA] = color2k.parseToHsla(cyanPerplexityColor);
  return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(startH, startS, step * x, 1));
})());

const TAGS_PALETTE_TRANSPARENT = Object.freeze((() => {
  const step = 1 / TAGS_PALETTE_COLORS_NUM;
  return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(0, 0, 0, step * x));
})());

const TAGS_PALETTE_HACKER = Object.freeze((() => {
  const step = 1 / TAGS_PALETTE_COLORS_NUM;
  return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(120, step * x, step * x * 0.5, 1));
})());

const TAGS_PALETTES = Object.freeze({
  CLASSIC: TAGS_PALETTE_CLASSIC,
  PASTEL: TAGS_PALETTE_PASTEL,
  GRIM: TAGS_PALETTE_GRIM,
  DARK: TAGS_PALETTE_DARK,
  GRAY: TAGS_PALETTE_GRAY,
  CYAN: TAGS_PALETTE_CYAN,
  TRANSPARENT: TAGS_PALETTE_TRANSPARENT,
  HACKER: TAGS_PALETTE_HACKER,
  CUSTOM: 'CUSTOM',
});

const convertColorInPaletteFormat =  currentPalette => value => currentPalette[parseInt(dropStr(1)(value), 10)] ?? defaultTagColor;

const TAG_HOME_PAGE_LAYOUT = {
  DEFAULT: 'default',
  COMPACT: 'compact',
  WIDER: 'wider',
  WIDE: 'wide',
  EXTRA_WIDE: 'extra-wide',
}

const parseBinaryState = binaryStr => {
  if (!/^[01-]+$/.test(binaryStr)) {
    throw new Error('Invalid binary state: ' + binaryStr);
  }
  return binaryStr.split('').map(bit => bit === '1' ? true : bit === '0' ? false : null);
};

const processTagField = currentPalette => name => value => {
  if (name === 'color' && value.startsWith('%')) return convertColorInPaletteFormat(currentPalette)(value);
  if (name === 'hide') return true;
  if (name === 'auto-submit') return true;
  if (name === 'set-sources') return parseBinaryState(value);
  return value;
};

const tagLineRegex = /<(label|position|color|tooltip|target|hide|link|link-target|icon|dir|in-dir|fence|in-fence|fence-border-style|fence-border-color|fence-border-width|fence-width|set-mode|set-model|auto-submit|set-sources)(?::([^<>]*))?>/g;
const parseOneTagLine = currentPalette => line =>
  Array.from(line.matchAll(tagLineRegex)).reduce(
    (acc, match) => {
      const [fullMatch, field, value] = match;
      const processedValue = processTagField(currentPalette)(field)(value);
      return {
        ...acc,
        [_.camelCase(field)]: processedValue,
        text: acc.text.replace(fullMatch, '').replace(/\\n/g, '\n'),
      };
    },
    {
      text: line, 
      color: defaultTagColor, 
      target: TAG_CONTAINER_TYPE.NEW, 
      hide: false, 
      'link-target': '_self',
    }
  );

const parseTagsText = text => {
  const lines = text.split('\n').filter(tag => tag.trim().length > 0);
  const palette = getPalette(loadConfig()?.tagPalette);
  return lines.map(parseOneTagLine(palette)).map((x, i) => ({...x, originalIndex: i}));
};

const getTagsContainer = () => $c(tagsContainerCls);

const posFromTag = tag => Object.values(TAG_POSITION).includes(tag.position) ? tag.position : TAG_POSITION.BEFORE;

const applyTagToString = (tag, val, caretPos) => {
  const {text} = tag;
  const timeString = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
  const processedText = text.replace(/\$\$time\$\$/g, timeString);
  switch (posFromTag(tag)) {
    case TAG_POSITION.BEFORE:
      return `${processedText}${val}`;
    case TAG_POSITION.AFTER:
      return `${val}${processedText}`;
    case TAG_POSITION.CARET:
      return `${takeStr(caretPos)(val)}${processedText}${dropStr(caretPos)(val)}`;
    default:
      throw new Error(`Invalid position: ${tag.position}`);
  }
};

const getPromptAreaFromTagsContainer = tagsContainerEl => PP.getAnyPromptArea(tagsContainerEl.parent());

const getPalette = paletteName => {
  // Add this check for 'CUSTOM'
  if (paletteName === TAGS_PALETTES.CUSTOM) {
    // Use tagPaletteCustom from config or default if not found
    return loadConfigOrDefault()?.tagPaletteCustom ?? defaultConfig.tagPaletteCustom;
  }
  // Fallback to predefined palettes or CLASSIC as default
  const palette = TAGS_PALETTES[paletteName];
  // Check if palette is an array before returning, otherwise return default
  return Array.isArray(palette) ? palette : TAGS_PALETTES.CLASSIC;
}

const createTag = containerEl => isPreview => tag => {
  if (tag.hide) return null;

  const labelString = tag.label ?? tag.text;
  const isTagLight = color2k.getLuminance(tag.color) > loadConfig().tagLuminanceThreshold;
  const colorMod = isTagLight ? color2k.darken : color2k.lighten;
  const hoverBgColor = color2k.toRgba(colorMod(tag.color, 0.1));
  const borderColor = color2k.toRgba(colorMod(tag.color, loadConfig().tagTweakRichBorderColor ? 0.2 : 0.1));

  const clickHandler = evt => {
    debugLog('clicked', tag, evt);
    if (tag.link) return;
    if (tag.setMode) {
      switch (tag.setMode) {
        case 'pro':
          PP.getModeProButton().click();
          break;
        case 'deep-research':
        case 'dr':
          PP.getModeDeepResearchButton().click();
          break;
        default:
          throw new Error(`Invalid set-mode: ${tag.setMode}`);
      }
    }
    if (tag.setModel) {
      setTimeout(() => { // delay for model button to be available after setting mode
        const modelDescriptor = PP.getModelDescriptorFromId(tag.setModel);
        debugLog('[createTag] clickHandler: set model=', tag.setModel, ' modelDescriptor=', modelDescriptor);
        PP.doSelectModel(modelDescriptor.index);
      }, 50);
    }
    if (tag.setSources) {
      setTimeout(() => {
        PP.getAnySourcesButton().click();
        setTimeout(() => {
          PP.setSourcesSelectionListValues()(tag.setSources, {
            callback: () => {
              debugLogTags('[createTag] clickHandler: setSources callback');
              setTimeout(() => {
                PP.getAnySourcesButton().click();
              }, 5);
            },
          });
          debugLogTags('[createTag] clickHandler: setSources=', tag.setSources);
        }, 10);
      }, 80);
    }
    if (tag.autoSubmit) {
      setTimeout(() => {
        const submitButton = PP.submitButtonAny();
        debugLogTags('[createTag] clickHandler: submitButton=', submitButton);
        if (submitButton.length) {
          if (submitButton.length > 1) {
            debugLogTags('[createTag] clickHandler: multiple submit buttons found, using first one');
          }
          submitButton.first().click();
        } else {
          debugLogTags('[createTag] clickHandler: no submit button found');
        }
      }, 300);
    }
    const el = jq(evt.currentTarget);
    const tagsContainer = el.closest(`.${tagsContainerCls}`);
    if (!tagsContainer.length) {
      debugLogTags('[clickHandler] no tags container found');
      return;
    }
    const promptArea = getPromptAreaFromTagsContainer(tagsContainer);
    if (!promptArea.length) {
      debugLogTags('[clickHandler] no prompt area found', promptArea);
      return;
    }
    const promptAreaRaw = promptArea[0];
    const newText = applyTagToString(tag, promptArea.val(), promptAreaRaw.selectionStart);
    changeValueUsingEvent(promptAreaRaw, newText);
    promptAreaRaw.focus();
  };

  const tagFont = loadConfig().tagFont;

  const defaultTooltip = tag.link? `${logPrefix} Open link: ${tag.link}` : `${logPrefix} Insert \`${tag.text}\` at position \`${posFromTag(tag)}\``;
  const tagEl = jq(`<div/>`)
    .addClass(tagCls)
    .prop('title', tag.tooltip ?? defaultTooltip)
    .attr('data-tag', JSON.stringify(tag))
    .css({
      backgroundColor: tag.color,
      borderColor,
      fontFamily: tagFont,
      borderRadius: `${loadConfig().tagRoundness}px`,
    })
    .attr('data-color', color2k.toHex(tag.color))
    .attr('data-hoverBgColor', color2k.toHex(hoverBgColor))
    .attr('data-font', tagFont)
    .on('mouseenter', event => {
      jq(event.currentTarget).css('background-color', hoverBgColor);
    })
    .on('mouseleave', event => {
      jq(event.currentTarget).css('background-color', tag.color);
    });

  if (isTagLight) {
    tagEl.addClass(tagDarkTextCls);
  }

  if (loadConfig()?.tagTweakNoBorder) {
    tagEl.addClass(tagTweakNoBorderCls);
  }
  if (loadConfig()?.tagTweakSlimPadding) {
    tagEl.addClass(tagTweakSlimPaddingCls);
  }
  if (loadConfig()?.tagTweakTextShadow) {
    tagEl.addClass(tagTweakTextShadowCls);
  }

  const textEl = jq('<span/>')
    .text(labelString)
    .css({  
      'font-weight': loadConfig().tagBold ? 'bold' : 'normal',
      'font-style': loadConfig().tagItalic ? 'italic' : 'normal', 
      'font-size': `${loadConfig().tagFontSize}px`,
      'transform': `translateY(${loadConfig().tagTextYOffset}px)`,
    });

  if (tag.icon) {
    const iconEl = jq('<img/>')
      .attr('src', getIconUrl(tag.icon)) 
      .addClass(tagIconCls)
      .css({
        'width': `${loadConfig().tagIconSize}px`,
        'height': `${loadConfig().tagIconSize}px`,
        'transform': `translateY(${loadConfig().tagIconYOffset}px)`,  
      });
    if (!labelString) {
      iconEl.css({
        marginLeft: '0',
        marginRight: '0',
      });
    }
    textEl.prepend(iconEl);
  }

  tagEl.append(textEl);

  if (tag.link) {
    const linkEl = jq('<a/>')
      .attr('href', tag.link)
      .attr('target', tag.linkTarget)
      .css({
        textDecoration: 'none',
        color: 'inherit'
      });
    textEl.wrap(linkEl);
  }

  if (!isPreview && !tag.link && !tag.dir) {
    tagEl.click(clickHandler);
  }
  containerEl.append(tagEl);

  return tagEl;
};

const genDebugFakeTags = () =>
  _.times(TAGS_PALETTE_COLORS_NUM, x => `Fake ${x} ${_.times(x / 3).map(() => 'x').join('')}<color:%${x % TAGS_PALETTE_COLORS_NUM}>`)
    .join('\n');

const getTagContainerType = containerEl => {
  if (containerEl.hasClass(threadTagContainerCls) || containerEl.hasClass(tagsPreviewThreadCls)) return TAG_CONTAINER_TYPE.THREAD;
  if (containerEl.hasClass(newTagContainerCls) || containerEl.hasClass(tagsPreviewNewCls)) return TAG_CONTAINER_TYPE.NEW;
  if (containerEl.hasClass(newTagContainerInCollectionCls) || containerEl.hasClass(tagsPreviewNewInCollectionCls)) return TAG_CONTAINER_TYPE.NEW_IN_COLLECTION;
  return null;
}

const getPromptWrapperTagContainerType = promptWrapper => {
  if (PP.getPromptAreaOfNewThread(promptWrapper).length) return TAG_CONTAINER_TYPE.NEW;
  if (PP.getPromptAreaOnThread(promptWrapper).length) return TAG_CONTAINER_TYPE.THREAD;
  if (PP.getPromptAreaOnCollection(promptWrapper).length) return TAG_CONTAINER_TYPE.NEW_IN_COLLECTION;
  return null;
};

const isTagRelevantForContainer = containerType => tag =>
  containerType === tag.target
|| (containerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION && tag.target === TAG_CONTAINER_TYPE.NEW)
|| tag.target === TAG_CONTAINER_TYPE.ALL

const tagContainerTypeToTagContainerClass = {
  [TAG_CONTAINER_TYPE.THREAD]: threadTagContainerCls,
  [TAG_CONTAINER_TYPE.NEW]: newTagContainerCls,
  [TAG_CONTAINER_TYPE.NEW_IN_COLLECTION]: newTagContainerInCollectionCls,
};

const currentUrlIsSettingsPage = () => window.location.pathname.includes('/settings/');

const refreshTags = ({force = false} = {}) => {
  const promptWrapper = PP.getPromptAreaWrapperOfNewThread()
    .add(PP.getPromptAreaWrapperOnThread())
    .add(PP.getPromptAreaWrapperOnCollection())
    .filter((_, rEl) => {
      const isPreview = Boolean(jq(rEl).attr('data-preview'));
      return isPreview || !currentUrlIsSettingsPage();
  });
  if (!promptWrapper.length) {
    debugLogTags('no prompt area found');
  }
  // debugLogTags('promptWrappers', promptWrapper);
  const allTags = _.flow(
    x => x + (unsafeWindow.phFakeTags ? `${nl}${genDebugFakeTags()}${nl}` : ''),
    parseTagsText,
  )(loadConfig()?.tagsText ?? defaultConfig.tagsText);
  debugLogTags('refreshing allTags', allTags);

  const createContainer = (promptWrapper) => {
    const el = jq(`<div/>`).addClass(tagsContainerCls);
    const tagContainerType = getPromptWrapperTagContainerType(promptWrapper);
    if (tagContainerType) {
      const clsToAdd = tagContainerTypeToTagContainerClass[tagContainerType];
      if (!clsToAdd) {
        console.error('Unexpected tagContainerType:', tagContainerType, {promptWrapper});
      }
      el.addClass(clsToAdd);
    }
    return el;
  }
  promptWrapper.each((_, rEl) => {
    const el = jq(rEl);
    if (el.parent().find(`.${tagsContainerCls}`).length) {
      el.parent().addClass(queryBoxCls);
      return;
    }
    el.before(createContainer(el));
  });

  const currentPalette = getPalette(loadConfig().tagPalette);

  const createFence = (fence) => {
    const fenceEl = jq('<div/>')
      .addClass(tagFenceCls)
      .css({
        'border-style': fence.fenceBorderStyle ?? 'solid',
        'border-color': fence.fenceBorderColor?.startsWith('%') 
            ? convertColorInPaletteFormat(currentPalette)(fence.fenceBorderColor) 
            : fence.fenceBorderColor ?? defaultTagColor,
        'border-width': fence.fenceBorderWidth ?? '1px',
      })
      .attr('data-tag', JSON.stringify(fence))
    ;
    const fenceContentEl = jq('<div/>')
      .addClass(tagFenceContentCls)
      .css({
        'width': fence.fenceWidth ?? '',
      })
    ;
    fenceEl.append(fenceContentEl);
    return { fenceEl, fenceContentEl };
  };

  const createDirectory = () => {
    const directoryEl = jq('<div/>').addClass(tagDirectoryCls);
    const directoryContentEl = jq('<div/>').addClass(tagDirectoryContentCls);
    directoryEl.append(directoryContentEl);
    return { directoryEl, directoryContentEl };
  };

  const containerEls = getTagsContainer();
  containerEls.each((_i, rEl) => {
    const containerEl = jq(rEl);
    const isPreview = Boolean(containerEl.attr('data-preview'));

    const tagContainerTypeFromPromptWrapper = getPromptWrapperTagContainerType(containerEl.nthParent(2));
    const prelimTagContainerType = getTagContainerType(containerEl);
    if (tagContainerTypeFromPromptWrapper !== prelimTagContainerType && !isPreview) {
      debugLog('tagContainerTypeFromPromptWrapper !== prelimTagContainerType', {tagContainerTypeFromPromptWrapper, prelimTagContainerType, containerEl, isPreview});
      containerEl
        .empty()
        .removeClass(threadTagContainerCls, newTagContainerCls, newTagContainerInCollectionCls)
        .addClass(tagContainerTypeToTagContainerClass[tagContainerTypeFromPromptWrapper])
      ;
    } else {
      if (!isPreview) {
        debugLogTags('tagContainerTypeFromPromptWrapper === prelimTagContainerType', {tagContainerTypeFromPromptWrapper, prelimTagContainerType, containerEl, isPreview});
      }
    }

    // TODO: use something else than lodash/fp. in following functions it behaved randomly very weirdly 
    // e.g. partial application of map resulting in an empty array or sortBy sorting field name instead 
    // of input array. possibly inconsistent normal FP order of arguments
    const mapParseAttrTag = xs => xs.map(el => JSON.parse(el.dataset.tag));
    const sortByOriginalIndex = xs => [...xs].sort((a, b) => a.originalIndex - b.originalIndex);
    const tagElsInCurrentContainer = containerEl.find(`.${tagCls}, .${tagFenceCls}`).toArray();
    const filterOutHidden = filter(x => !x.hide);
    const currentTags = _.flow(
      mapParseAttrTag,
      sortByOriginalIndex,
      filterOutHidden,
      _.uniq,
    )(tagElsInCurrentContainer);
    const tagContainerType = getTagContainerType(containerEl);
    const tagsForThisContainer = _.flow(
      filter(isTagRelevantForContainer(tagContainerType)),
      filterOutHidden,
      sortByOriginalIndex,
    )(allTags);
    debugLogTags('tagContainerType =', tagContainerType, ', current tags =', currentTags, ', tagsForThisContainer =', tagsForThisContainer, ', tagElsInCurrentContainer =', tagElsInCurrentContainer);
    if (_.isEqual(currentTags, tagsForThisContainer) && !force) {
      debugLogTags('no tags changed');
      return;
    }
    const diff = jsondiffpatch.diff(currentTags, tagsForThisContainer);
    const changedTags = jsondiffpatch.formatters.console.format(diff);
    debugLogTags('changedTags', changedTags);
    containerEl.empty();
    const tagHomePageLayout = loadConfig()?.tagHomePageLayout;
    if (!isPreview) {
      if ((tagContainerType === TAG_CONTAINER_TYPE.NEW || tagContainerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION)) {
        if (tagContainerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION) {
          // only compact layout is supported for new in collection
          if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) {
            containerEl.addClass(tagContainerCompactCls);
          }
        } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) {
          containerEl.addClass(tagContainerCompactCls);
        } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDER) {
          containerEl.addClass(tagContainerWiderCls);
        } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDE) {
          containerEl.addClass(tagContainerWideCls);
        } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.EXTRA_WIDE) {
          containerEl.addClass(tagContainerExtraWideCls);
        } else {
          containerEl.removeClass(`${tagContainerCompactCls} ${tagContainerWiderCls} ${tagContainerWideCls} ${tagContainerExtraWideCls}`);
        }
      }
    }

    const fences = {};
    const directories = {};

    const fencesWrapperEl = jq('<div/>').addClass(tagAllFencesWrapperCls);
    const restWrapperEl = jq('<div/>').addClass(tagRestOfTagsWrapperCls);

    tagsForThisContainer.forEach(tag => {
      const { fence, dir, inFence, inDir } = tag;
    
      const getOrCreateDirectory = dirName => {
        if (!directories[dirName]) directories[dirName] = createDirectory();
        return directories[dirName];
      };
      
      const getTagContainer = () => {
        if (fence) {
          if (!fences[fence]) fences[fence] = createFence(tag);
          return fences[fence].fenceContentEl;
        } else if (dir && inFence) {
          if (!fences[inFence]) {
            console.error(`fence ${inFence} for tag not found`, tag);
            return null;
          }
          const { directoryEl } = getOrCreateDirectory(dir);
          fences[inFence].fenceContentEl.append(directoryEl);
          return directoryEl;
        } else if (dir) {
          const { directoryEl } = getOrCreateDirectory(dir);
          restWrapperEl.append(directoryEl);
          return directoryEl;
        } else if (inFence) {
          if (!fences[inFence]) {
            console.error(`fence ${inFence} for tag not found`, tag);
            return null;
          }
          return fences[inFence].fenceContentEl;
        } else if (inDir) {
          if (!directories[inDir]) {
            console.error(`directory ${inDir} for tag not found`, tag);
            return null;
          }
          return directories[inDir].directoryContentEl;
        } else {
          return restWrapperEl;
        }
      };
      
      const tagContainer = getTagContainer();
      if (tagContainer && !fence) {
        createTag(tagContainer)(isPreview)(tag);
      }
    });

    Object.values(fences).forEach(({ fenceEl }) => fencesWrapperEl.append(fenceEl));
    containerEl.append(fencesWrapperEl).append(restWrapperEl);
  });
}

const setupTags = () => {
  debugLog('setting up tags');
  setInterval(refreshTags, 500);
}

const ICON_REPLACEMENT_MODE = Object.freeze({
  OFF: 'Off',
  LUCIDE1: 'Lucide 1',
  LUCIDE2: 'Lucide 2',
  LUCIDE3: 'Lucide 3',
  TDESIGN1: 'TDesign 1',
  TDESIGN2: 'TDesign 2',
  TDESIGN3: 'TDesign 3',
});

const leftPanelIconMappingsToLucide1 = Object.freeze({
  'search': 'search',
  'discover': 'telescope',
  'collection-2': 'shapes',
  'library': 'library-big',
});

const leftPanelIconMappingsToLucide2 = Object.freeze({
  'search': 'house',
  'discover': 'compass',
  'collection-2': 'square-stack',
  'library': 'archive',
});

const leftPanelIconMappingsToLucide3 = Object.freeze({
  'search': 'search',
  'discover': 'telescope',
  'collection-2': 'bot',
  'library': 'folder-open',
});

const leftPanelIconMappingsToTDesign1 = Object.freeze({
  'search': 'search',
  'discover': 'compass-filled',
  'collection-2': 'grid-view',
  'library': 'book',
});

const leftPanelIconMappingsToTDesign2 = Object.freeze({
  'search': 'search',
  'discover': 'shutter-filled',
  'collection-2': 'palette-1',
  'library': 'folder-open-1-filled',
});

const leftPanelIconMappingsToTDesign3 = Object.freeze({
  'search': 'search',
  'discover': 'banana-filled',
  'collection-2': 'chili-filled',
  'library': 'barbecue-filled',
});

const iconMappings = {
  LUCIDE1: leftPanelIconMappingsToLucide1,
  LUCIDE2: leftPanelIconMappingsToLucide2,
  LUCIDE3: leftPanelIconMappingsToLucide3,
  TDESIGN1: leftPanelIconMappingsToTDesign1,
  TDESIGN2: leftPanelIconMappingsToTDesign2,
  TDESIGN3: leftPanelIconMappingsToTDesign3,
};

const MODEL_LABEL_TEXT_MODE = Object.freeze({
  OFF: 'Off',
  FULL_NAME: 'Full Name',
  SHORT_NAME: 'Short Name',
  PP_MODEL_ID: 'PP Model ID',
  OWN_NAME_VERSION_SHORT: 'Own Name + Version Short',
});

const MODEL_LABEL_STYLE = Object.freeze({
  OFF: 'Off',
  JUST_TEXT: 'Just Text',
  BUTTON_SUBTLE: 'Button Subtle',
  BUTTON_WHITE: 'Button White',
  BUTTON_CYAN: 'Button Cyan',
});

const defaultConfig = Object.freeze({
  showCopilot: true,
  showCopilotNewThread: true,
  showCopilotRepeatLast: true,
  showCopilotCopyPlaceholder: true,
  tagsText: '',
  debugMode: false,
  debugTagsMode: false,
  tagPalette: 'CLASSIC',
  tagPaletteCustom: ['#000', '#fff', '#ff0', '#f00', '#0f0', '#00f', '#0ff', '#f0f'],
  tagFont: 'Roboto',
  tagHomePageLayout: TAG_HOME_PAGE_LAYOUT.DEFAULT,
  tagLuminanceThreshold: 0.35,
  tagBold: false,
  tagItalic: false,
  tagFontSize: 16,
  tagIconSize: 16,
  tagRoundness: 4,
  tagTextYOffset: 0,
  tagIconYOffset: 0,
  replaceIconsInMenu: ICON_REPLACEMENT_MODE.OFF,
  slimLeftMenu: false,
  hideHomeWidgets: false,
  modelLabelUseIconForReasoningModels: false,
  hideDiscoverButton: false,
  fixImageGenerationOverlay: false,
  extraSpaceBellowLastAnswer: false,
  modelLabelTextMode: MODEL_LABEL_TEXT_MODE.OFF,
  modelLabelStyle: MODEL_LABEL_STYLE.OFF,
  customModelPopover: false,
  mainCaptionHtml: '',
});

// TODO: if still using local storage, at least it should be prefixed with user script name
const storageKey = 'checkBoxStates';

const loadConfig = () => {
  try {
    // TODO: use storage from GM API
    const val = JSON.parse(localStorage.getItem(storageKey));
    // debugLog('loaded config', val);
    return val;
  } catch (e) {
    console.error('Failed to load config, using default', e);
    return defaultConfig;
  }
}

const loadConfigOrDefault = () => loadConfig() ?? defaultConfig

const saveConfig = cfg => {
  debugLog('saving config', cfg);
  localStorage.setItem(storageKey, JSON.stringify(cfg));
};

const createCheckbox = (id, labelText, onChange) => {
  debugLog("createCheckbox", id);
  const checkbox = jq(`<input type="checkbox" id=${id}>`);
  const label = jq(`<label class="checkbox_label" for="${id}">${labelText}</label>`);
  const checkboxWithLabel = jq('<div class="checkbox_wrapper"></div>').append(checkbox).append(' ').append(label);
  debugLog('checkboxwithlabel', checkboxWithLabel);

  getSettingsLastTabGroupContent().append(checkboxWithLabel);
  checkbox.on('change', onChange);
  return checkbox;
};

const createTextArea = (id, labelText, onChange, helpText, links) => {
  debugLog("createTextArea", id);
  const textarea = jq(`<textarea id=${id}></textarea>`);
  const bookIconHtml = `<img src="${getLucideIconUrl('book-text')}" class="w-4 h-4 invert inline-block"/>`;
  const labelTextHtml = `<span class="opacity-100">${labelText}</span>`;
  const label = jq(`<label class="textarea_label">${labelTextHtml}${helpText ? ' ' + bookIconHtml : ''}</label>`);
  const labelWithLinks = jq('<div/>').addClass('flex flex-row gap-2 mb-2').append(label);
  const textareaWrapper = jq('<div class="textarea_wrapper"></div>').append(labelWithLinks);
  if (links) {
    links.forEach(({icon, label, url, tooltip}) => {
      const iconHtml = `<img src="${getIconUrl(icon)}" class="w-4 h-4 invert opacity-50 hover:opacity-100 transition-opacity duration-300 ease-in-out"/>`;
      const link = jq(`<a href="${url}" target="_blank" class="flex flex-row gap-2 items-center">${icon ? iconHtml : ''}${label ? ' ' + label : ''}</a>`);
      link.attr('title', tooltip);
      labelWithLinks.append(link);
    });
  }
  if (helpText) {
    const help = jq(`<div/>`).addClass(helpTextCls).html(markdownConverter.makeHtml(helpText)).append(jq('<br/>'));
    help.find('a').each((_, a) => jq(a).attr('target', '_blank'));
    help.append(jq('<button/>').text('[Close help]').on('click', () => help.hide()));
    textareaWrapper.append(help);
    label
      .css({cursor: 'pointer'})
      .on('click', () => help.toggle())
      .prop('title', 'Click to toggle help')
    ;
    help.hide();
  }
  textareaWrapper.append(textarea);
  debugLog('textareaWithLabel', textareaWrapper);

  getSettingsLastTabGroupContent().append(textareaWrapper);
  textarea.on('change', onChange);
  return textarea;
};

const createSelect = (id, labelText, options, onChange) => {
  const select = jq(`<select id=${id}>`);
  options.forEach(({value, label}) => {
    jq('<option>').val(value).text(label).appendTo(select);
  });
  const label = jq(`<label class="select_label">${labelText}</label>`);
  const selectWithLabel = jq('<div class="select_wrapper"></div>').append(select).append(label);
  debugLog('selectWithLabel', selectWithLabel);

  getSettingsLastTabGroupContent().append(selectWithLabel);
  select.on('change', onChange);
  return select;
};

const createPaletteLegend = paletteName => {
  const wrapper = jq('<div/>')
    .addClass(tagPaletteCls)
    .append(jq('<span>').html('Palette of color codes:&nbsp;'))
  ;
  const palette = getPalette(paletteName);
  palette.forEach((color, i) => {
    const colorCode = `%${i}`;
    const colorPart = genColorPart(colorCode);
    // console.log('createPaletteLegend', {i, colorCode, colorPart, color});
    jq('<span/>')
      .text(colorCode)
      .addClass(tagPaletteItemCls)
      .css({
        'background-color': color,
      })
      .prop('title', `Copy ${colorPart} to clipboard`)
      .click(() => {
        copyTextToClipboard(colorPart);
      })
      .appendTo(wrapper);
  });
  return wrapper;
}

const createColorInput = (id, labelText, onChange) => {
  debugLog("createColorInput", id);
  const input = jq(`<input type="color" id=${id}>`);
  const label = jq(`<label class="color_label">${labelText}</label>`);
  const inputWithLabel = jq('<div class="color_wrapper"></div>').append(input).append(label);
  debugLog('inputWithLabel', inputWithLabel);

  getSettingsLastTabGroupContent().append(inputWithLabel);
  input.on('change', onChange);
  return input;
}

const createNumberInput = (id, labelText, onChange, {step = 1, min = 0, max = 100} = {}) => {
  debugLog("createNumberInput", id);
  const input = jq(`<input type="number" id=${id}>`)
    .prop('step', step)
    .prop('min', min)
    .prop('max', max)
    ;
  const label = jq(`<label class="number_label">${labelText}</label>`);
  const inputWithLabel = jq('<div class="number_wrapper"></div>').append(input).append(label);
  debugLog('inputWithLabel', inputWithLabel);

  getSettingsLastTabGroupContent().append(inputWithLabel);
  input.on('change', onChange);
  return input;
};

const createTagsPreview = () => {
  const wrapper = jq('<div/>')
    .addClass(tagsPreviewCls)
    .append(jq('<div>').text('Preview').addClass('text-lg font-bold'))
    .append(jq('<div>').text('Target New:'))
    .append(jq('<div>').addClass(tagsPreviewNewCls).addClass(tagsContainerCls).attr('data-preview', 'true'))
    .append(jq('<div>').text('Target Thread:'))
    .append(jq('<div>').addClass(tagsPreviewThreadCls).addClass(tagsContainerCls).attr('data-preview', 'true'))
  ;
  getSettingsLastTabGroupContent().append(wrapper);
}

const coPilotNewThreadAutoSubmitCheckboxId = 'coPilotNewThreadAutoSubmit';
const getCoPilotNewThreadAutoSubmitCheckbox = () => $i(coPilotNewThreadAutoSubmitCheckboxId);

const coPilotRepeatLastAutoSubmitCheckboxId = 'coPilotRepeatLastAutoSubmit';
const getCoPilotRepeatLastAutoSubmitCheckbox = () => $i(coPilotRepeatLastAutoSubmitCheckboxId);

const hideSideMenuCheckboxId = 'hideSideMenu';
const getHideSideMenuCheckbox = () => $i(hideSideMenuCheckboxId);

const tagsTextAreaId = 'tagsText';
const getTagsTextArea = () => $i(tagsTextAreaId);

const tagColorPickerId = genCssName('tagColorPicker');
const getTagColorPicker = () => $i(tagColorPickerId);

const enableDebugCheckboxId = genCssName('enableDebug');
const getEnableDebugCheckbox = () => $i(enableDebugCheckboxId);

const enableTagsDebugCheckboxId = genCssName('enableTagsDebug');
const getEnableTagsDebugCheckbox = () => $i(enableTagsDebugCheckboxId);

const tagPaletteSelectId = genCssName('tagPaletteSelect');
const getTagPaletteSelect = () => $i(tagPaletteSelectId);

const tagFontSelectId = genCssName('tagFontSelect');
const getTagFontSelect = () => $i(tagFontSelectId);

const tagTweakNoBorderCheckboxId = genCssName('tagTweakNoBorder');
const getTagTweakNoBorderCheckbox = () => $i(tagTweakNoBorderCheckboxId);

const tagTweakSlimPaddingCheckboxId = genCssName('tagTweakSlimPadding');
const getTagTweakSlimPaddingCheckbox = () => $i(tagTweakSlimPaddingCheckboxId);

const tagTweakRichBorderColorCheckboxId = genCssName('tagTweakRichBorderColor');
const getTagTweakRichBorderColorCheckbox = () => $i(tagTweakRichBorderColorCheckboxId);

const tagTweakTextShadowCheckboxId = genCssName('tagTweakTextShadow');
const getTagTweakTextShadowCheckbox = () => $i(tagTweakTextShadowCheckboxId);

const tagHomePageLayoutSelectId = genCssName('tagHomePageLayout');
const getTagHomePageLayoutSelect = () => $i(tagHomePageLayoutSelectId);

const tagLuminanceThresholdInputId = genCssName('tagLuminanceThreshold');
const getTagLuminanceThresholdInput = () => $i(tagLuminanceThresholdInputId);

const tagBoldCheckboxId = genCssName('tagBold');
const getTagBoldCheckbox = () => $i(tagBoldCheckboxId);

const tagItalicCheckboxId = genCssName('tagItalic');  
const getTagItalicCheckbox = () => $i(tagItalicCheckboxId);

const tagFontSizeInputId = genCssName('tagFontSize');
const getTagFontSizeInput = () => $i(tagFontSizeInputId);

const tagIconSizeInputId = genCssName('tagIconSize');  
const getTagIconSizeInput = () => $i(tagIconSizeInputId);

const tagRoundnessInputId = genCssName('tagRoundness');
const getTagRoundnessInput = () => $i(tagRoundnessInputId);

const tagTextYOffsetInputId = genCssName('tagTextYOffset');
const getTagTextYOffsetInput = () => $i(tagTextYOffsetInputId);

const tagIconYOffsetInputId = genCssName('tagIconYOffset');
const getTagIconYOffsetInput = () => $i(tagIconYOffsetInputId);

const tagPaletteCustomTextAreaId = genCssName('tagPaletteCustomTextArea');
const getTagPaletteCustomTextArea = () => $i(tagPaletteCustomTextAreaId);

const replaceIconsInMenuId = genCssName('replaceIconsInMenu');
const getReplaceIconsInMenu = () => $i(replaceIconsInMenuId);

const slimLeftMenuCheckboxId = genCssName('slimLeftMenu');
const getSlimLeftMenuCheckbox = () => $i(slimLeftMenuCheckboxId);

const hideHomeWidgetsCheckboxId = genCssName('hideHomeWidgets');
const getHideHomeWidgetsCheckbox = () => $i(hideHomeWidgetsCheckboxId);

const hideDiscoverButtonCheckboxId = genCssName('hideDiscoverButton');
const getHideDiscoverButtonCheckbox = () => $i(hideDiscoverButtonCheckboxId);

const fixImageGenerationOverlayCheckboxId = genCssName('fixImageGenerationOverlay');
const getFixImageGenerationOverlayCheckbox = () => $i(fixImageGenerationOverlayCheckboxId);

const extraSpaceBellowLastAnswerCheckboxId = genCssName('extraSpaceBellowLastAnswer');
const getExtraSpaceBellowLastAnswerCheckbox = () => $i(extraSpaceBellowLastAnswerCheckboxId);

const modelLabelTextModeSelectId = genCssName('modelLabelTextModeSelect');
const getModelLabelTextModeSelect = () => $i(modelLabelTextModeSelectId);

const modelLabelStyleSelectId = genCssName('modelLabelStyleSelect');
const getModelLabelStyleSelect = () => $i(modelLabelStyleSelectId);

const modelLabelOverwriteCyanIconToGrayCheckboxId = genCssName('modelLabelOverwriteCyanIconToGray');
const getModelLabelOverwriteCyanIconToGrayCheckbox = () => $i(modelLabelOverwriteCyanIconToGrayCheckboxId);

const modelLabelUseIconForReasoningModelsCheckboxId = genCssName('modelLabelUseIconForReasoningModels');
const getModelLabelUseIconForReasoningModelsCheckbox = () => $i(modelLabelUseIconForReasoningModelsCheckboxId);

const customModelPopoverCheckboxId = genCssName('customModelPopoverCheckbox');
const getCustomModelPopoverCheckbox = () => $i(customModelPopoverCheckboxId);

const mainCaptionHtmlTextAreaId = genCssName('mainCaptionHtmlTextArea');
const getMainCaptionHtmlTextArea = () => $i(mainCaptionHtmlTextAreaId);

const copyTextToClipboard = async text => {
  try {
    await navigator.clipboard.writeText(text);
    console.log('Text copied to clipboard', {text});
  } catch (err) {
    console.error('Failed to copy text: ', err);
  }
};

const genColorPart = color => `<color:${color}>`;

const loadCurrentConfigToSettingsForm = () => {
  const savedStatesRaw = JSON.parse(localStorage.getItem(storageKey));
  if (savedStatesRaw === null) { return; }
  const savedStates = {...defaultConfig, ...savedStatesRaw};

  getCoPilotNewThreadAutoSubmitCheckbox().prop('checked', savedStates.coPilotNewThreadAutoSubmit);
  getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked', savedStates.coPilotRepeatLastAutoSubmit);
  getHideSideMenuCheckbox().prop('checked', savedStates.hideSideMenu);
  getTagsTextArea().val(savedStates.tagsText);
  getEnableDebugCheckbox().prop('checked', savedStates.enableDebug);
  getEnableTagsDebugCheckbox().prop('checked', savedStates.debugTagsMode);
  getTagPaletteSelect().val(savedStates.tagPalette);
  getTagFontSelect().val(savedStates.tagFont);
  getTagTweakNoBorderCheckbox().prop('checked', savedStates.tagTweakNoBorder);
  getTagTweakSlimPaddingCheckbox().prop('checked', savedStates.tagTweakSlimPadding);
  getTagTweakRichBorderColorCheckbox().prop('checked', savedStates.tagTweakRichBorderColor);
  getTagTweakTextShadowCheckbox().prop('checked', savedStates.tagTweakTextShadow);
  getTagHomePageLayoutSelect().val(savedStates.tagHomePageLayout);
  getTagLuminanceThresholdInput().val(savedStates.tagLuminanceThreshold);
  getTagBoldCheckbox().prop('checked', savedStates.tagBold);
  getTagItalicCheckbox().prop('checked', savedStates.tagItalic);
  getTagFontSizeInput().val(savedStates.tagFontSize);
  getTagIconSizeInput().val(savedStates.tagIconSize);
  getTagTextYOffsetInput().val(savedStates.tagTextYOffset);
  getTagIconYOffsetInput().val(savedStates.tagIconYOffset);
  getTagRoundnessInput().val(savedStates.tagRoundness);
  getReplaceIconsInMenu().val(savedStates.replaceIconsInMenu);
  getSlimLeftMenuCheckbox().prop('checked', savedStates.slimLeftMenu);
  getHideHomeWidgetsCheckbox().prop('checked', savedStates.hideHomeWidgets);
  getHideDiscoverButtonCheckbox().prop('checked', savedStates.hideDiscoverButton);
  getFixImageGenerationOverlayCheckbox().prop('checked', savedStates.fixImageGenerationOverlay);
  getExtraSpaceBellowLastAnswerCheckbox().prop('checked', savedStates.extraSpaceBellowLastAnswer);
  getModelLabelTextModeSelect().val(savedStates.modelLabelTextMode);
  getModelLabelStyleSelect().val(savedStates.modelLabelStyle);
  getModelLabelOverwriteCyanIconToGrayCheckbox().prop('checked', savedStates.modelLabelOverwriteCyanIconToGray);
  getModelLabelUseIconForReasoningModelsCheckbox().prop('checked', savedStates.modelLabelUseIconForReasoningModels);
  getCustomModelPopoverCheckbox().prop('checked', savedStates.customModelPopover);
  getTagPaletteCustomTextArea().val((savedStates.tagPaletteCustom || []).join(', '));
  getMainCaptionHtmlTextArea().val(savedStates.mainCaptionHtml);
}

function handleSettingsInit() {
  const modalExists = getPerplexityHelperModal().length > 0;
  const firstCheckboxExists = getCoPilotNewThreadAutoSubmitCheckbox().length > 0;

  if (!modalExists || firstCheckboxExists) { return; }

  const $tabButtons = $c(modalTabGroupTabsCls).addClass('flex gap-2 items-end');

  const setActiveTab = (tabName) => {
    $c(modalTabGroupTabsCls).find('> button').each((_, tab) => {
      const $tab = jq(tab);
      if ($tab.attr('data-tab') === tabName) {
        $tab.addClass(modalTabGroupActiveCls);
      } else {
        $tab.removeClass(modalTabGroupActiveCls);
      }
    });
    $c(modalTabGroupContentCls).each((_, tab) => {
      const $tab = jq(tab);
      if ($tab.attr('data-tab') === tabName) {
        $tab.show();
      } else {
        $tab.hide();
      }
    });
  }

  const createTabContent = (tabName, tabLabel) => {
    const $tabButton = jq('<button/>').text(tabLabel).attr('data-tab', tabName).on('click', () => setActiveTab(tabName));
    $tabButtons.append($tabButton);
    const $tabContent = jq('<div/>')
      .addClass(modalTabGroupContentCls)
      .attr('data-tab', tabName);
    getSettingsModalContent().append($tabContent);
    return $tabContent;
  };

  const insertSeparator = () => getSettingsLastTabGroupContent().append('<hr/>');

  // -------------------------------------------------------------------------------------------------------------------
  createTabContent('general', 'General');

  createCheckbox(coPilotNewThreadAutoSubmitCheckboxId, 'Auto Submit New Thread With CoPilot', saveConfigFromForm);
  createCheckbox(coPilotRepeatLastAutoSubmitCheckboxId, 'Auto Submit Repeat With CoPilot', saveConfigFromForm);
  createCheckbox(hideSideMenuCheckboxId, 'Hide Side Menu', saveConfigFromForm);

  createSelect(
    replaceIconsInMenuId,
    'Replace menu icons',
    Object.values(ICON_REPLACEMENT_MODE).map(value => ({value, label: value})),
    () => {
      saveConfigFromForm();
      replaceIconsInMenu();
    }
  );

  createCheckbox(slimLeftMenuCheckboxId, 'Slim Left Menu', saveConfigFromForm);
  createCheckbox(hideHomeWidgetsCheckboxId, 'Hide Home Page Widgets', saveConfigFromForm);
  createCheckbox(hideDiscoverButtonCheckboxId, 'Hide Discover Button', saveConfigFromForm);
  createCheckbox(fixImageGenerationOverlayCheckboxId, 'Fix Image Generation Overlay Position (Experimental; only use if you encounter the submit button in a custom image prompt outside of the viewport)', saveConfigFromForm);
  createCheckbox(extraSpaceBellowLastAnswerCheckboxId, 'Add extra space bellow last answer', saveConfigFromForm);
  createSelect(
    modelLabelStyleSelectId,
    'Model Label Style',
    Object.values(MODEL_LABEL_STYLE).map(value => ({ value, label: value })),
    saveConfigFromForm
  );
  createSelect(
    modelLabelTextModeSelectId,
    'Model Label Text',
    Object.values(MODEL_LABEL_TEXT_MODE).map(value => ({ value, label: value })),
    saveConfigFromForm
  );
  createCheckbox(modelLabelOverwriteCyanIconToGrayCheckboxId, 'Overwrite Model Icon: Cyan -> Gray', saveConfigFromForm);
  createCheckbox(modelLabelUseIconForReasoningModelsCheckboxId, 'Use icon for reasoning models', saveConfigFromForm);
  createCheckbox(customModelPopoverCheckboxId, 'Custom Model Popover (Experimental)', saveConfigFromForm);
  createTextArea(mainCaptionHtmlTextAreaId, 'Main Caption HTML', saveConfigFromForm);
  // -------------------------------------------------------------------------------------------------------------------
  createTabContent('tags', 'Tags');
  
  createTextArea(tagsTextAreaId, 'Tags', saveConfigFromForm, tagsHelpText, [
    {icon: 'l:images', tooltip: 'Lucide Icons', url: 'https://lucide.dev/icons'},
    {icon: 'td:image', tooltip: 'TDesign Icons', url: 'https://tdesign.tencent.com/design/icon-en#header-69'}
  ])
    .prop('rows', 12).css('min-width', '700px').prop('wrap', 'off');

  const paletteLegendContainer = jq('<div/>').attr('id', 'palette-legend-container');
  getSettingsLastTabGroupContent().append(paletteLegendContainer);
  
  const updatePaletteLegend = () => {
    paletteLegendContainer.empty().append(createPaletteLegend(loadConfig()?.tagPalette));
  };
  
  updatePaletteLegend();

  createSelect(
    tagPaletteSelectId,
    'Tag color palette',
    Object.keys(TAGS_PALETTES).map(key => ({value: key, label: key})),
    () => {
      saveConfigFromForm();
      updatePaletteLegend();
      refreshTags();
    }
  );

  createTextArea(
    tagPaletteCustomTextAreaId,
    'Custom Palette Colors (comma-separated):',
    () => {
      saveConfigFromForm();
      // Update legend and tags only if CUSTOM is the selected palette
      if (getTagPaletteSelect().val() === TAGS_PALETTES.CUSTOM) {
        updatePaletteLegend();
        refreshTags();
      }
    }
  ).prop('rows', 2); // Make it a bit smaller than the main tags text area

  createTagsPreview();

  const FONTS = Object.keys(fontUrls);

  createSelect(
    tagFontSelectId,
    'Tag font',
    FONTS.map(font => ({ value: font, label: font })),
    () => {
      saveConfigFromForm();
      loadFont(loadConfigOrDefault().tagFont);
      refreshTags({force: true});
    }
  );
  createColorInput(tagColorPickerId, 'Custom color - copy field for tag to clipboard', () => {
    const color = getTagColorPicker().val();
    debugLog('color', color);
    copyTextToClipboard(genColorPart(color));
  });
  const saveConfigFromFormAndForceRefreshTags = () => {
    saveConfigFromForm();
    refreshTags({force: true});
  };

  createCheckbox(tagBoldCheckboxId, 'Bold text', saveConfigFromFormAndForceRefreshTags);
  createCheckbox(tagItalicCheckboxId, 'Italic text', saveConfigFromFormAndForceRefreshTags);
  
  createNumberInput(
    tagFontSizeInputId,
    'Font size',
    saveConfigFromFormAndForceRefreshTags,
    { min: 4, max: 64 }  
  );

  createNumberInput(
    tagIconSizeInputId,  
    'Icon size',
    saveConfigFromFormAndForceRefreshTags,
    { min: 4, max: 64 }
  );

  createNumberInput(
    tagRoundnessInputId,
    'Tag Roundness (px)',
    saveConfigFromFormAndForceRefreshTags,
    { min: 0, max: 32 }
  );

  createNumberInput(
    tagTextYOffsetInputId,
    'Text Y offset',  
    saveConfigFromFormAndForceRefreshTags,
    { step: 1, min: -50, max: 50 }
  );

  createNumberInput(  
    tagIconYOffsetInputId,
    'Icon Y offset',
    saveConfigFromFormAndForceRefreshTags, 
    { step: 1, min: -50, max: 50 }
  );

  createCheckbox(tagTweakNoBorderCheckboxId, 'No border', saveConfigFromFormAndForceRefreshTags);
  createCheckbox(tagTweakSlimPaddingCheckboxId, 'Slim padding', saveConfigFromFormAndForceRefreshTags);
  createCheckbox(tagTweakRichBorderColorCheckboxId, 'Rich Border Color', saveConfigFromFormAndForceRefreshTags);
  createCheckbox(tagTweakTextShadowCheckboxId, 'Text shadow', saveConfigFromFormAndForceRefreshTags);
  createNumberInput(
    tagLuminanceThresholdInputId, 
    'Tag Luminance Threshold (determines if tag is light or dark)', 
    saveConfigFromFormAndForceRefreshTags,
    {step: 0.01, min: 0, max: 1}
  );
  createSelect(
    tagHomePageLayoutSelectId, 
    'Tag container layout on home page (requires page refresh)',
    Object.values(TAG_HOME_PAGE_LAYOUT).map(value => ({value, label: value})),
    saveConfigFromForm
  );

  const $modelsList = jq('<div/>').text('Model IDs: ');
  const modelIds = PP.modelDescriptors.map(md => md.ppModelId).join(', ');
  $modelsList.append(modelIds);
  getSettingsLastTabGroupContent().append($modelsList);

  // -------------------------------------------------------------------------------------------------------------------
  createTabContent('settings', 'Settings');

  getSettingsLastTabGroupContent().append(jq('<div/>').text('Settings are stored in your browser\'s local storage. It is recommended to backup your settings via the export button below after every change.'));

  const buttonsContainer = jq('<div/>').addClass('flex gap-2');
  getSettingsLastTabGroupContent().append(buttonsContainer);

  const createExportButton = () => {
    const exportButton = jq('<button>')
        .text('Export Settings')
        .on('click', () => {
            const settings = JSON.stringify(getSavedStates(), null, 2);
            const blob = new Blob([settings], { type: 'application/json' });
            const date = new Date().toISOString().replace(/[:]/g, '-').replace(/T/g, '--').split('.')[0]; // Format: YYYY-MM-DD--HH-MM-SS
            const filename = `perplexity-helper-settings_${date}.json`;
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        });
    buttonsContainer.append(exportButton);
  };

  const createImportButton = () => {
    const importButton = jq('<button>')
      .text('Import Settings')
      .on('click', () => {
        const input = jq('<input type="file" accept=".json">');
        input.on('change', async (event) => {
          const file = event.target.files[0];
          if (file) {
            const confirmOverwrite = confirm('This will overwrite your current settings. Do you want to continue?');
            if (confirmOverwrite) {
              const reader = new FileReader();
              reader.onload = (e) => {
                try {
                    const importedSettings = JSON.parse(e.target.result);
                    saveConfig(importedSettings);
                    loadCurrentConfigToSettingsForm();
                    alert('Settings imported successfully!');
                } catch (error) {
                    console.error('Failed to import settings:', error);
                    alert('Failed to import settings. Please ensure the file is valid JSON.');
                }
              };
              reader.readAsText(file);
            }
          }
        });
        input.click();
      });
    buttonsContainer.append(importButton);
  };

  createExportButton();
  createImportButton();

  // -------------------------------------------------------------------------------------------------------------------
  createTabContent('debug', 'Debug'); // debug options at the bottom (do NOT add more normal options bellow this!)

  createCheckbox(enableDebugCheckboxId, 'Enable Debug', saveConfigFromForm);
  createCheckbox(enableTagsDebugCheckboxId, 'Enable tags debug log', saveConfigFromForm);

  // -------------------------------------------------------------------------------------------------------------------
  setActiveTab('general');
  loadCurrentConfigToSettingsForm();
}

debugLog(jq.fn.jquery);
const getSavedStates = () => JSON.parse(localStorage.getItem(storageKey));

const getModal = () => jq("[data-testid='quick-search-modal'] > div");
const getCopilotToggleButton = textarea => textarea.parent().parent().find('[data-testid="copilot-toggle"]');
const upperControls = () => jq('svg[data-icon="lock"] ~ div:contains("Share")').nthParent(5).closest('.flex.justify-between:not(.grid-cols-3)');

const getControlsArea = () => jq('textarea[placeholder="Ask follow-up"]').parent().parent().children().last();

const getCopilotNewThreadButton = () => jq('#copilot_new_thread');
const getCopilotRepeatLastButton = () => jq('#copilot_repeat_last');
const getSelectAllButton = () => jq('#perplexity_helper_select_all');
const getSelectAllAndSubmitButton = () => jq('#perplexity_helper_select_all_and_submit');
const getCopyPlaceholder = () => jq('#perplexity_helper_copy_placeholder');
const getCopyAndFillInPlaceholder = () => jq('#perplexity_helper_copy_placeholder_and_fill_in');
const getTopSettingsButtonEl = () => $i(topSettingsButtonId);
const getLeftSettingsButtonEl = () => $i(leftSettingsButtonId);
const getSideMenu = () => jq('.min-h-\\[100vh\\]').children().first();
const getSettingsModalContent = () => getPerplexityHelperModal().find(`.modal-content`);
const getSettingsLastTabGroupContent = () => getSettingsModalContent().find(`.${modalTabGroupContentCls}`).last();

const getSubmitBtn0 = () => jq('svg[data-icon="arrow-up"]').last().parent().parent();
const getSubmitBtn1 = () => jq('svg[data-icon="arrow-right"]').last().parent().parent();
const getSubmitBtn2 = () => jq('svg[data-icon="code-fork"]').last().parent().parent();

const isStandardControlsAreaFc = () => !getControlsArea().hasClass('bottom-0');
const getCurrentControlsArea = () => isStandardControlsAreaFc() ? getControlsArea() : getControlsArea().find('.bottom-0');

const getDashedCheckboxButton = () => jq('svg[data-icon="square-dashed"]').parent().parent();
const getStarSVG = () => jq('svg[data-icon="star-christmas"]');
const getSpecifyQuestionBox = () => jq('svg[data-icon="star-christmas"]').parent().parent().parent().last();

const getNumberOfDashedSVGs = () => getSpecifyQuestionBox().find('svg[data-icon="square-dashed"]').length;
const getSpecifyQuestionControlsWrapper = () => getSpecifyQuestionBox().find('button:contains("Continue")').parent()
const getCopiedModal = () => jq('#copied-modal');
const getCopiedModal2 = () => jq('#copied-modal-2');
const getCopyPlaceholderInput = () => getSpecifyQuestionBox().find('textarea');

const getSubmitButton0or2 = () => getSubmitBtn0().length < 1 ? getSubmitBtn2() : getSubmitBtn0();

const questionBoxWithPlaceholderExists = () => getSpecifyQuestionBox().find('textarea')?.attr('placeholder')?.length > 0 ?? false;

// TODO: no longer used? was this for agentic questions?
const selectAllCheckboxes = () => {
  const currentCheckboxes = getDashedCheckboxButton();
  debugLog('checkboxes', currentCheckboxes);

  const removeLastObject = (arr) => {
    if (!_.isEmpty(arr)) {
      debugLog('arr', arr);
      const newArr = _.dropRight(arr, 1);
      debugLog("newArr", newArr);
      getDashedCheckboxButton().last().click();

      return setTimeout(() => {
        removeLastObject(newArr)
      }, 1)

    }
  };

  removeLastObject(currentCheckboxes);
}

const isCopilotOn = (el) => el.hasClass('text-super')

const toggleBtnDot = (btnDot, value) => {
  debugLog(' toggleBtnDot btnDot', btnDot);

  const btnDotInner = btnDot.find('.rounded-full');

  debugLog('btnDotInner', btnDotInner);

  if (!btnDotInner.hasClass('bg-super') && value === true) {
    btnDot.click();
  }
}

const checkForCopilotToggleState = (timer, checkCondition, submitWhenTrue, submitButtonVersion) => {
  debugLog("checkForCopilotToggleState run", timer, checkCondition(), submitWhenTrue, submitButtonVersion);
  if (checkCondition()) {
    clearInterval(timer);
    debugLog("checkForCopilotToggleState condition met, interval cleared");
    const submitBtn = submitButtonVersion === 0 ? getSubmitButton0or2() : getSubmitBtn1();

    debugLog('submitBtn', submitBtn);
    if (submitWhenTrue) {
      submitBtn.click();
    }
  }
}

const openNewThreadModal = (lastQuery) => {
  debugLog('openNewThreadModal', lastQuery)

  const newThreadText = jq(".sticky div").filter(function () {
    return /^New Thread$/i.test(jq(this).text());
  });
  if (!newThreadText.length) {
    debugLog('newThreadText.length should be 1', newThreadText.length);
    return;
  }
  debugLog('newThreadText', newThreadText);

  newThreadText.click();
  setTimeout(() => {
      debugLog('newThreadText.click()');
      const modal = getModal();

      if (modal.length > 0) {
        const textArea = modal.find('textarea');
        if (textArea.length !== 1) debugLog('textArea.length should be 1', textArea.length);

        const newTextArea = textArea.last();
        const textareaElement = newTextArea[0];
        debugLog('textareaElement', textareaElement);
        changeValueUsingEvent(textareaElement, lastQuery);

        const copilotButton = getCopilotToggleButton(newTextArea);

        toggleBtnDot(copilotButton, true);
        const isCopilotOnBtn = () => isCopilotOn(copilotButton);

        const coPilotNewThreadAutoSubmit =
          getSavedStates()
            ? getSavedStates().coPilotNewThreadAutoSubmit
            : getCoPilotNewThreadAutoSubmitCheckbox().prop('checked');

        const copilotCheck = () => {
          const ctx = {timer: null};
          ctx.timer = setInterval(() => checkForCopilotToggleState(ctx.timer, isCopilotOnBtn, coPilotNewThreadAutoSubmit, 1), 500);
        }

        copilotCheck();
      } else {
        debugLog('else of modal.length > 0');
      }
    },
    2000);
}

const getLastQuery = () => {
  // wrapper around prompt + response
  const lastQueryBox = jq('svg[data-icon="repeat"]').last().nthParent(7);
  if (lastQueryBox.length === 0) {
    debugLog('lastQueryBox not found');
  }

  const wasCopilotUsed = lastQueryBox.find('svg[data-icon="star-christmas"]').length > 0;
  const lastQueryBoxText = lastQueryBox.find('.whitespace-pre-line').text();

  debugLog('[getLastQuery]', {lastQueryBox, wasCopilotUsed, lastQueryBoxText});
  return lastQueryBoxText ?? null;
}

const saveConfigFromForm = () => {
  const checkBoxStates = {
    coPilotNewThreadAutoSubmit: getCoPilotNewThreadAutoSubmitCheckbox().prop('checked'),
    coPilotRepeatLastAutoSubmit: getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked'),
    hideSideMenu: getHideSideMenuCheckbox().prop('checked'),
    tagsText: getTagsTextArea().val(),
    enableDebug: getEnableDebugCheckbox().prop('checked'),
    debugTagsMode: getEnableTagsDebugCheckbox().prop('checked'),
    tagPalette: getTagPaletteSelect().val(),
    tagFont: getTagFontSelect().val(),
    tagTweakNoBorder: getTagTweakNoBorderCheckbox().prop('checked'),
    tagTweakSlimPadding: getTagTweakSlimPaddingCheckbox().prop('checked'),
    tagTweakRichBorderColor: getTagTweakRichBorderColorCheckbox().prop('checked'),
    tagTweakTextShadow: getTagTweakTextShadowCheckbox().prop('checked'),
    tagHomePageLayout: getTagHomePageLayoutSelect().val(),
    tagLuminanceThreshold: parseFloat(getTagLuminanceThresholdInput().val()),
    tagBold: getTagBoldCheckbox().prop('checked'),
    tagItalic: getTagItalicCheckbox().prop('checked'),
    tagFontSize: parseInt(getTagFontSizeInput().val()),
    tagIconSize: parseInt(getTagIconSizeInput().val()),
    tagRoundness: parseInt(getTagRoundnessInput().val()),
    tagTextYOffset: parseInt(getTagTextYOffsetInput().val()),
    tagIconYOffset: parseInt(getTagIconYOffsetInput().val()),
    replaceIconsInMenu: getReplaceIconsInMenu().val(),
    slimLeftMenu: getSlimLeftMenuCheckbox().prop('checked'),
    hideHomeWidgets: getHideHomeWidgetsCheckbox().prop('checked'),
    hideDiscoverButton: getHideDiscoverButtonCheckbox().prop('checked'),
    fixImageGenerationOverlay: getFixImageGenerationOverlayCheckbox().prop('checked'),
    extraSpaceBellowLastAnswer: getExtraSpaceBellowLastAnswerCheckbox().prop('checked'),
    modelLabelTextMode: getModelLabelTextModeSelect().val(),
    modelLabelStyle: getModelLabelStyleSelect().val(),
    modelLabelOverwriteCyanIconToGray: getModelLabelOverwriteCyanIconToGrayCheckbox().prop('checked'),
    modelLabelUseIconForReasoningModels: getModelLabelUseIconForReasoningModelsCheckbox().prop('checked'),
    customModelPopover: getCustomModelPopoverCheckbox().prop('checked'),
    tagPaletteCustom: getTagPaletteCustomTextArea().val().split(',').map(s => s.trim()).filter(Boolean),
    mainCaptionHtml: getMainCaptionHtmlTextArea().val(),
  };
  saveConfig(checkBoxStates);
};

const showPerplexityHelperSettingsModal = () => {
  loadCurrentConfigToSettingsForm();
  getPerplexityHelperModal().show().css('display', 'flex');
}

const hidePerplexityHelperSettingsModal = () => {
  getPerplexityHelperModal().hide();
}

const handleTopSettingsButtonInsertion = () => {
  const copilotHelperSettings = getTopSettingsButtonEl();
  // TODO: no longer works
  // debugLog('upperControls().length > 0', upperControls().length, 'copilotHelperSettings.length', copilotHelperSettings.length, 'upperControls().children().length', upperControls().children().length);
  if (upperControls().length > 0 && copilotHelperSettings.length < 1 && upperControls().children().length >= 1) {
    debugLog('inserting settings button');
    upperControls().children().eq(0).children().eq(0).append(upperButton(topSettingsButtonId, cogIco, 'Perplexity Helper Settings'));
  }
};

const handleTopSettingsButtonSetup = () => {
  const settingsButtonEl = getTopSettingsButtonEl();

  if (settingsButtonEl.length === 1 && !settingsButtonEl.attr('data-has-custom-click-event')) {
    debugLog('handleTopSettingsButtonSetup: setting up the button');
    if (settingsButtonEl.length === 0) {
      debugLog('handleTopSettingsButtonSetup: settingsButtonEl.length === 0');
    }

    settingsButtonEl.on("click", () => {
      debugLog('perplexity_helper_settings open click');
      showPerplexityHelperSettingsModal();
    });

    settingsButtonEl.attr('data-has-custom-click-event', true);
  }
};

const applySideMenuHiding = () => {
  const sideMenu = getSideMenu();
  if (getSavedStates()) getSavedStates().hideSideMenu || getHideSideMenuCheckbox().prop('checked') ? sideMenu.hide() : sideMenu.show();
};

const handleModalCreation = () => {
  if (getPerplexityHelperModal().length > 0) return;
  debugLog('handleModalCreation: creating modal');
  jq("body").append(modalHTML);

  getPerplexityHelperModal().find('.close').on('click', () => {
    debugLog('perplexity_helper_settings close  click');
    hidePerplexityHelperSettingsModal();
  });
};

const lucideIconMappings = {
  LUCIDE1: leftPanelIconMappingsToLucide1,
  LUCIDE2: leftPanelIconMappingsToLucide2,
};

const findKeyByValue = (obj, value) => 
  Object.keys(obj).find(key => obj[key] === value);

const SUPPORTED_ICON_REPLACEMENT_MODES = [
  ICON_REPLACEMENT_MODE.LUCIDE1, 
  ICON_REPLACEMENT_MODE.LUCIDE2, 
  ICON_REPLACEMENT_MODE.LUCIDE3,
  ICON_REPLACEMENT_MODE.TDESIGN1,
  ICON_REPLACEMENT_MODE.TDESIGN2,
  ICON_REPLACEMENT_MODE.TDESIGN3,
];

const replaceIconsInMenu = () => {
  const config = loadConfigOrDefault();
  const replacementMode = findKeyByValue(ICON_REPLACEMENT_MODE, config.replaceIconsInMenu);

  if (SUPPORTED_ICON_REPLACEMENT_MODES.includes(config.replaceIconsInMenu)) {
    const processedAttr = `data-${pplxHelperTag}-processed`;
    const iconMapping = iconMappings[replacementMode];
    if (!iconMapping) {
      console.error(logPrefix, '[replaceIconsInMenu] iconMapping not found', { config, iconMappings });
      return;
    }

    const svgEls = PP.getIconsInLeftPanel().find('svg[data-icon]');
    // debugLog('[replaceIconsInMenu] svgEls', svgEls);
    svgEls.each((_, svgEl) => {
      const $svg = jq(svgEl);
      const processed = $svg.attr(processedAttr);
      if (processed) return;

      const iconName = $svg.attr('data-icon');
      debugLog('[replaceIconsInMenu] iconName', iconName);
      const replacementIconName = iconMapping[iconName];
      debugLog('[replaceIconsInMenu] replacementIconName', replacementIconName);

      $svg.attr(processedAttr, true);
      
      if (replacementIconName) {
        const isTDesign = config.replaceIconsInMenu.startsWith('TDesign');
        const newIconUrl = (isTDesign ? getTDesignIconUrl : getLucideIconUrl)(replacementIconName);

        debugLog('[replaceIconsInMenu] replacing icon', { iconName, replacementIconName, svgEl, newIconUrl });
        $svg.hide();
        const newIconEl = jq('<img>').attr('src', newIconUrl).addClass('invert opacity-50');
        if (isTDesign) newIconEl.addClass('h-6');
        $svg.parent().addClass(lucideIconParentCls);
        $svg.after(newIconEl);
      } else {
        if (!['plus', 'thread'].includes(iconName)) {
          console.error('[replaceIconsInMenu] no replacement icon found', { iconName, replacementIconName });
        }
      }
    });
  }
};

const handleLeftSettingsButtonSetup = () => {
  const existingLeftSettingsButton = getLeftSettingsButtonEl();
  if (existingLeftSettingsButton.length === 1) {
    const wrapper = existingLeftSettingsButton.parent();
    if (!wrapper.is(':last-child')) {
      wrapper.appendTo(wrapper.parent());
    }
    return;
  }

  const leftPanel = PP.getIconsInLeftPanel();

  if (leftPanel.length === 0) {
    debugLog('handleLeftSettingsButtonSetup: leftPanel not found');
  }

  const wrapperEl = jq('<div>').attr('id', leftSettingsButtonWrapperId);
  const iconEl = jq(cogIco);

  const btnEl = jq('<button>')
    .attr('id', leftSettingsButtonId)
    .attr('title', 'Perplexity Helper Settings')
    .addClass('text-textOff dark:text-textOffDark font-sans text-xs flex items-center gap-x-sm')
    .addClass('md:hover:bg-offsetPlus text-textOff dark:text-textOffDark md:hover:text-textMain dark:md:hover:bg-offsetPlusDark  dark:md:hover:text-textMainDark font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-in-out font-sans  select-none items-center relative group  justify-center text-center items-center rounded-full cursor-point active:scale-95 origin-center whitespace-nowrap inline-flex text-sm px-sm font-medium h-8')
    .append(iconEl)
    .append(jq('<span>').text('Perplexity Helper'))
  ;
  btnEl.on('click', () => {
    debugLog('left settings button clicked');
    showPerplexityHelperSettingsModal();
  });
  wrapperEl.append(btnEl);
  leftPanel.append(wrapperEl);
}

const handleSlimLeftMenu = () => {
  const config = loadConfigOrDefault();
  if (!config.slimLeftMenu) return;

  const leftPanel = PP.getLeftPanel();
  if (leftPanel.length === 0) {
    debugLog('handleSlimLeftMenu: leftPanel not found');
  }
  if (leftPanel.length !== 1) {
    console.warn(logPrefix, '[handleSlimLeftMenu] too many leftPanel found', leftPanel);
  }

  leftPanel.addClass(leftPanelSlimCls);
};

const handleHideHomeWidgets = () => {
  const config = loadConfigOrDefault();
  if (!config.hideHomeWidgets) return;

  const homeWidgets = PP.getHomeWidgets();
  if (homeWidgets.length === 0) {
    debugLog('handleHideHomeWidgets: homeWidgets not found');
    return;
  }
  if (homeWidgets.length > 1) {
    console.warn(logPrefix, '[handleHideHomeWidgets] too many homeWidgets found', homeWidgets);
  }

  homeWidgets.hide();
};

const handleFixImageGenerationOverlay = () => {
  const config = loadConfigOrDefault();
  if (!config.fixImageGenerationOverlay) return;

  const imageGenerationOverlay = PP.getImageGenerationOverlay();
  if (imageGenerationOverlay.length === 0) {
    // debugLog('handleFixImageGenerationOverlay: imageGenerationOverlay not found');
    return;
  }

  // only if wrench button is cyan (we are in custom prompt)
  if (!imageGenerationOverlay.find('button').hasClass('bg-super')) return;

  const transform = imageGenerationOverlay.css('transform');
  if (!transform) return;
  
  // Handle both matrix and translate formats
  const matrixMatch = transform.match(/matrix\(.*,\s*([\d.]+),\s*([\d.]+)\)/);
  const translateMatch = transform.match(/translate\(([\d.]+)px(?:,\s*([\d.]+)px)?\)/);
  
  const currentX = matrixMatch 
    ? matrixMatch[1]  // Matrix format: 5th value is X translation
    : translateMatch?.[1] || 0;  // Translate format: first value
  
  debugLog('[handleFixImageGenerationOverlay] currentX', currentX, 'transform', transform);
  imageGenerationOverlay.css({
    transform: `translate(${currentX}px, 0px)`
  });
};

const handleExtraSpaceBellowLastAnswer = () => {
  const config = loadConfigOrDefault();
  if (!config.extraSpaceBellowLastAnswer) return;
  jq('body')
    .find(`.erp-tab\\:rounded-none > .max-w-screen > .scrollable-container > .mx-auto > .mx-auto > .relative > div > div.bottom-mobileNavHeight`)
    .prev()
    .css({
      // backgroundColor: 'magenta',
      paddingBottom: '15em'
    })
  ;
};

const handleSearchPage = () => {
  const controlsArea = getCurrentControlsArea();
  controlsArea.addClass(controlsAreaCls);
  controlsArea.parent().find('textarea').first().addClass(textAreaCls);
  controlsArea.addClass(roundedMD);
  controlsArea.parent().addClass(roundedMD);


  if (controlsArea.length === 0) {
    debugLog('controlsArea not found', {
      controlsArea,
      currentControlsArea: getCurrentControlsArea(),
      isStandardControlsAreaFc: isStandardControlsAreaFc()
    });
  }

  const lastQueryBoxText = getLastQuery();

  const mainTextArea = isStandardControlsAreaFc() ? controlsArea.prev().prev() : controlsArea.parent().prev();

  if (mainTextArea.length === 0) {
    debugLog('mainTextArea not found', mainTextArea);
  }


  debugLog('lastQueryBoxText', { lastQueryBoxText });
  if (lastQueryBoxText) {
    const copilotNewThread = getCopilotNewThreadButton();
    const copilotRepeatLast = getCopilotRepeatLastButton();

    if (controlsArea.length > 0 && copilotNewThread.length < 1) {
      controlsArea.append(button('copilot_new_thread', robotIco, "Starts new thread for with last query text and Copilot ON", standardButtonCls));
    }

    // Due to updates in Perplexity, this is unnecessary for now
    // if (controlsArea.length > 0 && copilotRepeatLast.length < 1) {
    //   controlsArea.append(button('copilot_repeat_last', robotRepeatIco, "Repeats last query with Copilot ON"));
    // }

    if (!copilotNewThread.attr('data-has-custom-click-event')) {
      copilotNewThread.on("click", function () {
        debugLog('copilotNewThread Button clicked!');
        openNewThreadModal(getLastQuery());
      });
      copilotNewThread.attr('data-has-custom-click-event', true);
    }

    if (!copilotRepeatLast.attr('data-has-custom-click-event')) {
      copilotRepeatLast.on("click", function () {
        const controlsArea = getCurrentControlsArea();
        const textAreaElement = controlsArea.parent().find('textarea')[0];

        const coPilotRepeatLastAutoSubmit =
          getSavedStates()
            ? getSavedStates().coPilotRepeatLastAutoSubmit
            : getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked');

        debugLog('coPilotRepeatLastAutoSubmit', coPilotRepeatLastAutoSubmit);
        changeValueUsingEvent(textAreaElement, getLastQuery());
        const copilotToggleButton = getCopilotToggleButton(mainTextArea);
        debugLog('mainTextArea', mainTextArea);
        debugLog('copilotToggleButton', copilotToggleButton);

        toggleBtnDot(copilotToggleButton, true);
        const isCopilotOnBtn = () => isCopilotOn(copilotToggleButton);

        const copilotCheck = () => {
          const ctx = { timer: null };
          ctx.timer = setInterval(() => checkForCopilotToggleState(ctx.timer, isCopilotOnBtn, coPilotRepeatLastAutoSubmit, 0), 500);
        };

        copilotCheck();
        debugLog('copilot_repeat_last Button clicked!');
      });
      copilotRepeatLast.attr('data-has-custom-click-event', true);
    }
  }

  if (getNumberOfDashedSVGs() > 0 && getNumberOfDashedSVGs() === getDashedCheckboxButton().length
    && getSelectAllButton().length < 1 && getSelectAllAndSubmitButton().length < 1) {
    debugLog('getNumberOfDashedSVGs() === getNumberOfDashedSVGs()', getNumberOfDashedSVGs());
    debugLog('getSpecifyQuestionBox', getSpecifyQuestionBox());

    const specifyQuestionControlsWrapper = getSpecifyQuestionControlsWrapper();
    debugLog('specifyQuestionControlsWrapper', specifyQuestionControlsWrapper);
    const selectAllButton = textButton('perplexity_helper_select_all', 'Select all', 'Selects all options');
    const selectAllAndSubmitButton = textButton('perplexity_helper_select_all_and_submit', 'Select all & submit', 'Selects all options and submits');

    specifyQuestionControlsWrapper.append(selectAllButton);
    specifyQuestionControlsWrapper.append(selectAllAndSubmitButton);

    getSelectAllButton().on("click", function () {
      selectAllCheckboxes();
    });

    getSelectAllAndSubmitButton().on("click", function () {
      selectAllCheckboxes();
      setTimeout(() => {
        getSpecifyQuestionControlsWrapper().find('button:contains("Continue")').click();
      }, 200);
    });
  }

  const constructClipBoard = (buttonId, buttonGetter, modalGetter, copiedModalId, elementGetter) => {
    const placeholderValue = getSpecifyQuestionBox().find('textarea').attr('placeholder');

    const clipboardInstance = new ClipboardJS(`#${buttonId}`, {
      text: () => placeholderValue
    });

    const copiedModal = `<span id="${copiedModalId}">Copied!</span>`;
    debugLog('copiedModalId', copiedModalId);
    debugLog('copiedModal', copiedModal);

    jq('main').append(copiedModal);

    clipboardInstance.on('success', _ => {
      var buttonPosition = buttonGetter().position();
      jq(`#${copiedModalId}`).css({
        top: buttonPosition.top - 30,
        left: buttonPosition.left + 50
      }).show();

      if (elementGetter !== undefined) {
        changeValueUsingEvent(elementGetter()[0], placeholderValue);
      }

      setTimeout(() => {
        modalGetter().hide();
      }, 5000);
    });
  };

  if (questionBoxWithPlaceholderExists() && getCopyPlaceholder().length < 1) {
    const copyPlaceholder = textButton('perplexity_helper_copy_placeholder', 'Copy placeholder', 'Copies placeholder value');
    const copyPlaceholderAndFillIn = textButton('perplexity_helper_copy_placeholder_and_fill_in', 'Copy placeholder and fill in',
      'Copies placeholder value and fills in input');

    const specifyQuestionControlsWrapper = getSpecifyQuestionControlsWrapper();

    specifyQuestionControlsWrapper.append(copyPlaceholder);
    specifyQuestionControlsWrapper.append(copyPlaceholderAndFillIn);

    constructClipBoard('perplexity_helper_copy_placeholder', getCopyPlaceholder, getCopiedModal, 'copied-modal');
    constructClipBoard('perplexity_helper_copy_placeholder_and_fill_in', getCopyAndFillInPlaceholder, getCopiedModal2, 'copied-modal-2', getCopyPlaceholderInput);
  }
}

const getLabelFromModelDescription = modelLabelStyle => modelLabelFromAriaLabel => modelDescription => {
  if (!modelDescription) return modelLabelFromAriaLabel;
  switch (modelLabelStyle) {
  case MODEL_LABEL_TEXT_MODE.FULL_NAME:
    return modelDescription.nameEn;
  case MODEL_LABEL_TEXT_MODE.SHORT_NAME:
    return modelDescription.nameEnShort ?? modelDescription.nameEn;
  case MODEL_LABEL_TEXT_MODE.PP_MODEL_ID:
    return modelDescription.ppModelId;
  case MODEL_LABEL_TEXT_MODE.OWN_NAME_VERSION_SHORT:
    const nameText = modelDescription.ownNameEn ?? modelDescription.nameEn;
    const versionText = modelDescription.ownVersionEnShort ?? modelDescription.ownVersionEn;
    return [nameText, versionText].filter(Boolean).join(' ');
  default:
    throw new Error(`Unknown model label style: ${modelLabelStyle}`);
  }
}

const getExtraClassesFromModelLabelStyle = modelLabelStyle => {
  switch (modelLabelStyle) {
  case MODEL_LABEL_STYLE.BUTTON_SUBTLE:
    return modelLabelStyleButtonSubtleCls;
  case MODEL_LABEL_STYLE.BUTTON_WHITE:
    return modelLabelStyleButtonWhiteCls;
  case MODEL_LABEL_STYLE.BUTTON_CYAN:
    return modelLabelStyleButtonCyanCls;
  default:
    return '';
  }
}
const handleModelLabel = () => {
  const config = loadConfigOrDefault();
  if (!config.modelLabelStyle || config.modelLabelStyle === MODEL_LABEL_STYLE.OFF) return;

  const $modelIcons = PP.getAnyModelButton();
  $modelIcons.each((_, el) => {
    const $el = jq(el);
    if (!$el.find(`.${modelLabelCls}`).length) {
      $el.prepend(jq(`<span class="${modelLabelCls}"></span>`));
    }
    const $label = $el.find(`.${modelLabelCls}`);
    const modelDescription = PP.getModelDescriptionFromModelButton($el);
    const modelLabelFromAriaLabel = $el.attr('aria-label');
    const modelLabel = getLabelFromModelDescription(config.modelLabelTextMode)(modelLabelFromAriaLabel)(modelDescription);
    if (!modelLabel) {
      console.error('[handleModelLabel] modelLabel is null', { modelDescription, modelLabelFromAriaLabel, $el });
      return;
    }
    const extraClasses = [
      getExtraClassesFromModelLabelStyle(config.modelLabelStyle),
      config.modelLabelOverwriteCyanIconToGray ? modelLabelOverwriteCyanIconToGrayCls : '',
    ].filter(Boolean).join(' ');
    $label.attr('data-extra-classes', extraClasses);
    if ($label.text() !== modelLabel) {
      $label.attr('data-model-description', JSON.stringify(modelDescription));
      $label.text(modelLabel);
      $label.addClass(extraClasses);
    }
    if (config.modelLabelUseIconForReasoningModels) {
      if ($el.find(`.${reasoningModelCls}`).length === 0) {
        const iconUrl = getLucideIconUrl('lightbulb');
        $el.prepend(jq(`<img src="${iconUrl}" alt="Reasoning model" class="${reasoningModelCls}" />`));
      }
      const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`);
      const isReasoning = modelDescription?.modelType === 'reasoning';
      if (isReasoning) {
        $reasoningModelIcon.css({display: 'inline-block'});
        $label.css({padding:  '0 10px 0 0'});
        if (config.modelLabelStyle === MODEL_LABEL_STYLE.JUST_TEXT) {
          $reasoningModelIcon.addClass(iconColorGrayCls);
        } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_CYAN) {
          $reasoningModelIcon.addClass(iconColorCyanCls);
        } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_SUBTLE) {
          $reasoningModelIcon.addClass(iconColorGrayCls);
        } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_WHITE) {
          $reasoningModelIcon.addClass(iconColorWhiteCls);
        }
      } else {
        $reasoningModelIcon.remove();
        $label.css({padding:  '2px 4px 0 14px'});
      }
    }
  });
}

const handleHideDiscoverButton = () => {
  const config = loadConfigOrDefault();
  if (!config.hideDiscoverButton) return;
  const $iconsInLeftPanel = PP.getIconsInLeftPanel().find('div.space-y-xs.flex-1:has(a[href="/discover"])').children().eq(1);
  $iconsInLeftPanel.hide();
}

const handleCustomModelPopover = () => {
  const config = loadConfigOrDefault();
  if (!config.customModelPopover) return;

  const $modelSelectionList = PP.getModelSelectionList();
  if ($modelSelectionList.length === 0) return;
  const processedAttr = 'ph-processed-custom-model-popover';
  if ($modelSelectionList.attr(processedAttr)) return;
  $modelSelectionList.attr(processedAttr, true);
  $modelSelectionList.nthParent(2).css({maxHeight: 'initial'});
  $modelSelectionList.css({
    display: 'grid',
    gridTemplateColumns: '1fr 1fr',
    gap: '10px',
    'grid-auto-rows': 'min-content',
  });

  // Place items in first column
  $modelSelectionList.children(":nth-child(3)").css("grid-area", "1 / 1");
  $modelSelectionList.children(":nth-child(4)").css("grid-area", "2 / 1");
  $modelSelectionList.children(":nth-child(5)").css("grid-area", "3 / 1");
  $modelSelectionList.children(":nth-child(6)").css("grid-area", "4 / 1");

  // Place items in second column
  $modelSelectionList.children(":nth-child(8)").css("grid-area", "1 / 2");
  $modelSelectionList.children(":nth-child(9)").css("grid-area", "2 / 2");
  $modelSelectionList.children(":nth-child(10)").css("grid-area", "3 / 2");
  $modelSelectionList.children(":nth-child(11)").css("grid-area", "4 / 2");
  // $modelSelectionList.children(":nth-child(2)").css("grid-area", "5 / 2");
  $modelSelectionList.children(":nth-child(1)").css("grid-area", "5 / 2").css("border-top", "1px solid #343637");

  $modelSelectionList.children(":nth-child(2)").css("display", "none");
}

const mainCaptionAppliedCls = genCssName('mainCaptionApplied');
const handleMainCaptionHtml = () => {
  const config = loadConfigOrDefault();
  if (!config.mainCaptionHtml) return;
  if (PP.getMainCaption().hasClass(mainCaptionAppliedCls)) return;
  PP.setMainCaptionHtml(config.mainCaptionHtml);
  PP.getMainCaption().addClass(mainCaptionAppliedCls);
}

const work = () => {
  handleModalCreation();
  handleTopSettingsButtonInsertion();
  handleTopSettingsButtonSetup();
  handleSettingsInit();
  handleLeftSettingsButtonSetup();
  handleSlimLeftMenu();
  handleHideHomeWidgets();
  handleExtraSpaceBellowLastAnswer();
  applySideMenuHiding();
  replaceIconsInMenu();
  handleModelLabel();
  handleHideDiscoverButton();
  handleCustomModelPopover();
  handleMainCaptionHtml();
  const regex = /^https:\/\/www\.perplexity\.ai\/search\/?.*/;
  const currentUrl = jq(location).attr('href');
  const matchedCurrentUrlAsSearchPage = regex.test(currentUrl);

  // debugLog("currentUrl", currentUrl);
  // debugLog("matchedCurrentUrlAsSearchPage", matchedCurrentUrlAsSearchPage);

  if (matchedCurrentUrlAsSearchPage) {
    handleSearchPage();
  }
};

const fontUrls = {
  Roboto: 'https://fonts.cdnfonts.com/css/roboto',
  Montserrat: 'https://fonts.cdnfonts.com/css/montserrat',
  Lato: 'https://fonts.cdnfonts.com/css/lato',
  Oswald: 'https://fonts.cdnfonts.com/css/oswald-4',
  Raleway: 'https://fonts.cdnfonts.com/css/raleway-5',
  'Ubuntu Mono': 'https://fonts.cdnfonts.com/css/ubuntu-mono',
  Nunito: 'https://fonts.cdnfonts.com/css/nunito',
  Poppins: 'https://fonts.cdnfonts.com/css/poppins',
  'Playfair Display': 'https://fonts.cdnfonts.com/css/playfair-display',
  Merriweather: 'https://fonts.cdnfonts.com/css/merriweather',
  'Fira Sans': 'https://fonts.cdnfonts.com/css/fira-sans',
  Quicksand: 'https://fonts.cdnfonts.com/css/quicksand',
  Comfortaa: 'https://fonts.cdnfonts.com/css/comfortaa-3',
  'Almendra': 'https://fonts.cdnfonts.com/css/almendra',
  'Enchanted Land': 'https://fonts.cdnfonts.com/css/enchanted-land',
  'Cinzel Decorative': 'https://fonts.cdnfonts.com/css/cinzel-decorative',
  'Orbitron': 'https://fonts.cdnfonts.com/css/orbitron',
  'Exo 2': 'https://fonts.cdnfonts.com/css/exo-2',
  'Chakra Petch': 'https://fonts.cdnfonts.com/css/chakra-petch',
  'Open Sans Condensed': 'https://fonts.cdnfonts.com/css/open-sans-condensed',
  'Saira Condensed': 'https://fonts.cdnfonts.com/css/saira-condensed',
  Inter: 'https://cdn.jsdelivr.net/npm/@fontsource/[email protected]/index.min.css',
};

const loadFont = (fontName) => {
  const fontUrl = fontUrls[fontName];
  debugLog('loadFont', { fontName, fontUrl });
  if (fontUrl) {
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = fontUrl;
    document.head.appendChild(link);
  }
};

const setupFixImageGenerationOverlay = () => {
  const config = loadConfigOrDefault();
  if (config.fixImageGenerationOverlay) {
    setInterval(handleFixImageGenerationOverlay, 250);
  }
};

(function () {
  if (loadConfigOrDefault()?.enableDebug) {
    enableDebugMode();
  }

  debugLog('TAGS_PALETTES', TAGS_PALETTES);
  if (loadConfigOrDefault()?.debugTagsMode) {
    enableTagsDebugging();
  }

  'use strict';
  jq("head").append(`<style>${styles}</style>`);

  setupTags();
  setupFixImageGenerationOverlay();

  const mainInterval = setInterval(work, 1000);
  window.ph = {
    stopWork: () => { clearInterval(mainInterval); },
    work,
    jq,
    showPerplexityHelperSettingsModal,
    enableTagsDebugging: () => { debugTags = true; },
    disableTagsDebugging: () => { debugTags = false; },
  }

  loadFont(loadConfigOrDefault().tagFont);

  console.log(`%c${userscriptName}%c\n  %cTiartyos%c & %cmonnef%c\n ... loaded`,
    'color: #aaffaa; font-size: 1.5rem; background-color: rgba(0, 0, 0, 0.5); padding: 2px;', 
    '', 
    'color: #6b02ff; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;', 
    '', 
    'color: #aa2cc3; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;', 
    '',
    '');
}());

QingJ © 2025

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