Perplexity helper

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

目前為 2024-03-17 提交的版本,檢視 最新版本

// ==UserScript==
// @name        Perplexity helper
// @namespace   Tiartyos
// @match       https://www.perplexity.ai/*
// @grant       none
// @version     2.4
// @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
// @homepageURL https://www.perplexity.ai/
// @license     GPL-3.0-or-later
// ==/UserScript==

const jq = $.noConflict();
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 nl = '\n';
const markdownConverter = new showdown.Converter();

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

const logPrefix = '[Perplexity helper]';

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

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

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

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: white;" 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 modalHTML = `
<div id="${perplexityHelperModalId}" class="modal">
  <div class="modal-content">
    <span class="close">&times;</span>
    <h1>Perplexity Helper settings</h1>
  </div>
</div>
`;

const genCssName = x => `perplexity-helper--${x}`;

const tagsContainerCls = genCssName('tags-container');
const threadTagContainerCls = genCssName('thread-tag-container');
const newTagContainerCls = genCssName('new-tag-container');
const tagCls = genCssName('tag');
const tagDarkTextCls = genCssName('tag-dark-text');
const tagPaletteCls = genCssName('tag-palette');
const tagPaletteItemCls = genCssName('tag-palette-item');
const tagsPreviewCls = genCssName('tags-preview');
const tagsPreviewNewCls = genCssName('tags-preview-new');
const tagsPreviewThreadCls = genCssName('tags-preview-thread');
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 roundedMD = genCssName('rounded-md');

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

const styles = `
.checkbox_label {
  color: white;
}

.textarea_wrapper {
  display: flex;
  flex-direction: column;
}

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

.textarea_label {
  margin-right: auto;
}

.${helpTextCls} {
  max-width: 580px;
  background-color: #225;
  padding: 0.3em 0.7em;
  border-radius: 0.5em;
  margin: 1em 0;
}
.${helpTextCls} {
  cursor: text;
}
.${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);
}

.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 #888;
  background-color: #202025;
  border-radius: 6px;
  color: rgb(206, 206, 210);
  flex-direction: column;
  position: relative;
  row-gap: 10px;
  overflow-y: auto;
  cursor: default;
}

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

.modal-content h1 {
  margin-bottom: 0.5em;
  border-bottom: 1px solid #888;
}

.close {
  color: rgb(206, 206, 210);
  float: right;
  font-size: 28px;
  font-weight: bold;
  position: absolute;
  right: 10px;
  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;
  gap: 5px;
  margin: 5px 0;
  flex-wrap: wrap;
}
.${tagsContainerCls}.${threadTagContainerCls} {
  margin-left: 0.5em;
  margin-right: 0.5em;
  margin-bottom: 2px;
}

.${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: #D0D0D0;
}
.${tagCls}.${tagDarkTextCls} {
  color: #333333;
  text-shadow: 1px 0 0.5px white, -1px 0 0.5px white, 0 1px 0.5px white, 0 -1px 0.5px white;
}
.${tagCls} span {
  position: relative;
  top: 1.5px;
  text-shadow: 1px 0 0.5px black, -1px 0 0.5px black, 0 1px 0.5px black, 0 -1px 0.5px black;
}
.${tagCls}:hover {
  background-color: #333;
  color: #fff;
}
.${tagCls}.${tagDarkTextCls}:hover {
  /* color: #171717; */
  color: #2f2f2f;
}

.${tagPaletteCls} {
  display: flex;
  flex-wrap: wrap;
  gap: 1px;
}
.${tagPaletteCls} .${tagPaletteItemCls} {
  text-shadow: 0 0 4px black;
  width: 35px;
  height: 20px;
  display: inline-block;
  text-align: center;
  border-radius: 0.2em;
  padding: 0 2px;
  transition: color 0.2s;
}

.${tagPaletteItemCls}:hover {
  cursor: pointer;
  color: white;
}

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

.${tagsPreviewNewCls}:before {
  content: 'Target New: ';
}

.${tagsPreviewThreadCls}:before {
  content: 'Target Thread: ';
}

.${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;
}
`;

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

const TAG_CONTAINER_TYPE = {
  NEW: 'new',
  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)

---

Examples:
\`\`\`
Vintage Story -
stable diffusion web ui - <label:SDWU>
, prefer concise modern syntax and style, <position:caret><label:concise modern>
FFXIV: <color:%15><label:FFXIV>
tell me a joke<label:Joke><tooltip:>
\`\`\`
`.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 cyanButtonPerplexityColor = '#1fb8cd';

const TAGS_PALETTE_COLORS_NUM = 16;
const TAGS_PALETTE = Object.freeze((() => {
  const step = 360 / TAGS_PALETTE_COLORS_NUM;
  const [startH, startS, startL, startA] = color2k.parseToHsla(cyanButtonPerplexityColor);
  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));
})());

debugLogTags('TAGS_PALETTE', TAGS_PALETTE);

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

const processTagField = name => value => {
  if (name === 'color' && value.startsWith('%')) return convertColorInPaletteFormat(value);
  return value;
}

const tagLineRegex = /<(label|position|color|tooltip|target):([^<>]*)>/g;
const parseOneTagLine = (line) =>
  Array.from(line.matchAll(tagLineRegex)).reduce(
    (acc, match) => {
      const [fullMatch, field, value] = match;
      const processedValue = processTagField(field)(value);
      return {
        ...acc,
        [field]: processedValue,
        text: acc.text.replace(fullMatch, '').replace(/\\n/g, '\n'),
      };
    },
    {text: line, color: defaultTagColor, target: TAG_CONTAINER_TYPE.NEW}
  );

const parseTagsText = text => {
  const lines = text.split('\n').filter(tag => tag.trim().length > 0);
  return lines.map(parseOneTagLine);
}

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

const promptAreaOfNewThreadSelector = 'textarea[placeholder="Ask anything..."]';
const getPromptAreaOfNewThread = () => jq(promptAreaOfNewThreadSelector);
const getPromptAreaWrapperOfNewThread = () => getPromptAreaOfNewThread().parent().parent().parent().parent();

const promptAreaOnThreadSelector = 'textarea[placeholder="Ask follow-up"]';
const getPromptAreaOnThread = () => jq(promptAreaOnThreadSelector);
const getPromptAreaWrapperOnThread = () => getPromptAreaOnThread().parent().parent().parent().parent();

const anyPromptAreaSelector = `${promptAreaOfNewThreadSelector},${promptAreaOnThreadSelector}`;

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

const applyTagToString = (tag, val, caretPos) => {
  const {text} = tag;
  switch (posFromTag(tag)) {
    case TAG_POSITION.BEFORE:
      return `${text}${val}`;
    case TAG_POSITION.AFTER:
      return `${val}${text}`;
    case TAG_POSITION.CARET:
      return `${takeStr(caretPos)(val)}${text}${dropStr(caretPos)(val)}`;
    default:
      throw new Error(`Invalid position: ${tag.position}`);
  }
};

const getPromptAreaFromTagsContainer = tagsContainerEl => tagsContainerEl.parent().find(anyPromptAreaSelector);

const createTag = containerEl => isPreview => tag => {
  const labelString = tag.label ?? tag.text;
  const isTagLight = color2k.getLuminance(tag.color) > 0.35;
  const colorMod = isTagLight ? color2k.darken : color2k.lighten;
  const hoverBgColor = color2k.toRgba(colorMod(tag.color, 0.1));
  const borderColor = color2k.toRgba(colorMod(tag.color, 0.1));
  const clickHandler = evt => {
    debugLog('clicked', tag, evt);
    const el = jq(evt.currentTarget);
    const promptArea = getPromptAreaFromTagsContainer(el.parent());
    if (!promptArea.length) {
      debugLogTags('no prompt area found', promptArea);
      return;
    }
    const promptAreaRaw = promptArea[0];
    const newText = applyTagToString(tag, promptArea.val(), promptAreaRaw.selectionStart);
    changeValueUsingEvent(promptAreaRaw, newText);
    promptAreaRaw.focus();
  };
  const tagEl =
    jq(`<div/>`)
      .addClass(tagCls)
      .prop('title', tag.tooltip ?? `${logPrefix} Insert \`${tag.text}\` at position \`${posFromTag(tag)}\``)
      .attr('data-tag', JSON.stringify(tag))
      .click(isPreview ? null : clickHandler)
      .css({
        backgroundColor: tag.color,
        borderColor,
      })
      .attr('data-color', color2k.toHex(tag.color))
      .attr('data-hoverBgColor', color2k.toHex(hoverBgColor))
      .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);
  }
  const textEl = jq('<span/>')
    .text(labelString)
    .css({
      // TODO: either move tag text shadows to options or remove styles and this override
      textShadow: 'none'
    })
  ;

  tagEl.append(textEl);
  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;
  return null;
}

const isTagRelevantForContainer = containerType => tag => containerType === tag.target || tag.target === TAG_CONTAINER_TYPE.ALL

const refreshTags = () => {
  const promptWrapper = getPromptAreaWrapperOfNewThread().add(getPromptAreaWrapperOnThread());
  if (!promptWrapper.length) {
    debugLogTags('no prompt area found');
  }
  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);
    if (promptWrapper.find(promptAreaOnThreadSelector).length) {
      el.addClass(threadTagContainerCls);
    }
    if (promptWrapper.find(promptAreaOfNewThreadSelector).length) {
      el.addClass(newTagContainerCls);
    }
    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 containerEls = getTagsContainer();
  containerEls.each((_i, rEl) => {
    const containerEl = jq(rEl);
    const isPreview = Boolean(containerEl.attr('data-preview'));
    const currentTags = containerEl.find(`.${tagCls}`).map((i, el) => JSON.parse(el.dataset.tag)).toArray();
    const tagContainerType = getTagContainerType(containerEl);
    const tagsForThisContainer = allTags.filter(isTagRelevantForContainer(tagContainerType));
    debugLogTags('tagContainerType', tagContainerType, 'current tags', currentTags, 'tagsForThisContainer', tagsForThisContainer);
    if (_.isEqual(currentTags, tagsForThisContainer)) {
      debugLogTags('no tags changed');
      return;
    }
    containerEl.empty();
    tagsForThisContainer.forEach(createTag(containerEl)(isPreview));
  });
}

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

const defaultConfig = Object.freeze({
  showCopilot: true,
  showCopilotNewThread: true,
  showCopilotRepeatLast: true,
  showCopilotCopyPlaceholder: true,
  tagsText: '',
  debugMode: false,
});

// 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(label).append(checkbox);
  debugLog('checkboxwithlabel', checkboxWithLabel);

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

const createTextArea = (id, labelText, onChange, helpText) => {
  debugLog("createTextArea", id);
  const textarea = jq(`<textarea id=${id}></textarea>`);
  const label = jq(`<label class="textarea_label">${labelText}${helpText ? ' 📖' : ''}</label>`);
  const textareaWithLabel = jq('<div class="textarea_wrapper"></div>').append(label);
  if (helpText) {
    const help = jq(`<div/>`).addClass(helpTextCls).html(markdownConverter.makeHtml(helpText)).append(jq('<br/>'));
    help.append(jq('<button/>').text('[Close help]').on('click', () => help.hide()));
    textareaWithLabel.append(help);
    label
      .css({cursor: 'pointer'})
      .on('click', () => help.toggle())
      .prop('title', 'Click to toggle help')
    ;
    help.hide();
  }
  textareaWithLabel.append(textarea);
  debugLog('textareaWithLabel', textareaWithLabel);

  getSettingsModalContent().append(textareaWithLabel);
  textarea.on('change', onChange);
  return textarea;
}

const createPaletteLegend = () => {
  const wrapper = jq('<div/>')
    .addClass(tagPaletteCls)
    .append(jq('<span>').html('Palette of color codes:&nbsp;'))
  ;
  TAGS_PALETTE.forEach((color, i) => {
    const colorCode = `%${i}`;
    const colorPart = genColorPart(colorCode);
    jq('<span/>')
      .text(colorCode)
      .addClass(tagPaletteItemCls)
      .css({
        'background-color': color,
      })
      .prop('title', `Copy ${colorPart} to clipboard`)
      .click(() => {
        copyTextToClipboard(colorPart);
      })
      .appendTo(wrapper);
  });
  getSettingsModalContent().append(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(label).append(input);
  debugLog('inputWithLabel', inputWithLabel);

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

const createTagsPreview = () => {
  const wrapper = jq('<div/>')
    .addClass(tagsPreviewCls)
    .append(jq('<div>').html('Preview'))
    .append(jq('<div>').addClass(tagsPreviewNewCls).addClass(tagsContainerCls).attr('data-preview', 'true'))
    .append(jq('<div>').addClass(tagsPreviewThreadCls).addClass(tagsContainerCls).attr('data-preview', 'true'))
  ;
  getSettingsModalContent().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 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}>`;

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

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

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

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

  insertSeparator();

  createTextArea(tagsTextAreaId, 'Tags', saveConfigFromForm, tagsHelpText).prop('rows', 5).css('min-width', '700px').prop('wrap', 'off');
  createPaletteLegend();
  createColorInput(tagColorPickerId, 'Custom color - copy field for tag to clipboard:', () => {
    const color = getTagColorPicker().val();
    debugLog('color', color);
    copyTextToClipboard(genColorPart(color));
  });
  createTagsPreview();

  insertSeparator();

  createCheckbox(enableDebugCheckboxId, 'Enable Debug', saveConfigFromForm);

  const savedStates = JSON.parse(localStorage.getItem(storageKey));
  if (savedStates === null) { return; }

  getCoPilotNewThreadAutoSubmitCheckbox().prop('checked', savedStates.coPilotNewThreadAutoSubmit);
  getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked', savedStates.coPilotRepeatLastAutoSubmit);
  getHideSideMenuCheckbox().prop('checked', savedStates.hideSideMenu);
  getTagsTextArea().val(savedStates.tagsText);
  getEnableDebugCheckbox().prop('checked', savedStates.enableDebug);
}

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="plus"]').parent().parent().parent().parent().first();

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 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;

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'),
  };
  saveConfig(checkBoxStates);
};

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

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

const handleTopSettingsButtonInsertion = () => {
  const copilotHelperSettings = getTopSettingsButtonEl();
  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).after(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');
      showPerplexityHelperModal();
    });

    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');
    hidePerplexityHelperModal();
  });
};

const getLeftPanel = () => jq('svg[data-icon="rectangle-vertical-history"]').parent().parent().parent().parent().parent().parent();

const handleLeftSettingsButtonSetup = () => {
  if (getLeftSettingsButtonEl().length === 1) return;

  const leftPanel = getLeftPanel();

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

  const wrapperEl = jq('<div>')
    .addClass('flex items-center')
    .css({ paddingRight: '0.3em', justifyContent: 'right' })
  ;
  const iconEl = jq('svg[data-icon="gear"]').parent().clone();

  const btnEl = jq('<button>')
    .attr('id', leftSettingsButtonId)
    .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')
    .text('Perplexity Helper')
    .append(iconEl)
  ;
  btnEl.on('click', () => {
    debugLog('left settings button clicked');
    showPerplexityHelperModal();
  });
  wrapperEl.append(btnEl);
  leftPanel.append(wrapperEl);
}

const work = () => {
  handleModalCreation();
  handleTopSettingsButtonInsertion();
  handleTopSettingsButtonSetup();
  handleSettingsInit();
  handleLeftSettingsButtonSetup();

  applySideMenuHiding();

  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) {
    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)
    }


  }
};

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

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


  setupTags();

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

QingJ © 2025

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