// ==UserScript==
// @name Perplexity helper
// @namespace Tiartyos
// @match https://www.perplexity.ai/*
// @grant none
// @version 5.1
// @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/color2k@2.0.2/dist/index.unpkg.umd.js
// @require https://cdn.jsdelivr.net/npm/showdown@2.1.0/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/perplex-plus@0.0.18/dist/lib/perplex-plus.js
// @homepageURL https://www.perplexity.ai/
// @license GPL-3.0-or-later
// @downloadURL none
// ==/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) => ``;
const upperButton = (id, icoName, title) => `
`
const textButton = (id, text, title) => `
`
const icoColor = '#1F1F1F';
const robotIco = ``;
const robotRepeatIco = ``;
const cogIco = ``;
const perplexityHelperModalId = 'perplexityHelperModal';
const getPerplexityHelperModal = () => $i(perplexityHelperModalId);
const modalSettingsTitleCls = genCssName('modal-settings-title');
const gitlabLogo = classes => `
`;
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 = `
×
Changes may require page refresh.
`;
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 modelIconButtonCls = genCssName('model-icon-button');
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 iconColorGoldCls = genCssName('icon-color-gold');
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: #8D9191 !important;
}
button:hover > .${modelLabelCls}.${modelLabelStyleButtonWhiteCls} {
color: #fff !important;
}
.${modelIconButtonCls} svg[stroke] {
stroke: #8D9191 !important;
}
.${modelIconButtonCls}:hover svg[stroke] {
stroke: #fff !important;
}
button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}) {
background: #191A1A !important;
color: #2D2F2F !important;
}
button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}):hover {
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};
}
.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) svg[stroke] {
stroke: ${cyanPerplexityColor} !important;
}
.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}):hover svg[stroke] {
stroke: #fff !important;
}
button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}) {
color: #888 !important;
}
button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}):hover {
color: #fff !important;
}
.${reasoningModelCls} {
width: 16px;
height: 16px;
margin-right: 4px;
margin-left: 8px;
margin-top: -2px;
filter: invert();
}
button:has(.${reasoningModelCls}) > div > div > svg {
width: 32px;
height: 16px;
margin-left: 8px;
margin-right: 12px;
margin-top: 0px;
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;
}
button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) .${iconColorCyanCls} {
filter: invert(100%);
}
.${iconColorGrayCls} {
filter: invert(50%);
transition: filter 0.2s;
}
button:has(.${reasoningModelCls}):hover .${iconColorGrayCls} {
filter: invert(100%);
}
.${iconColorWhiteCls} {
filter: invert(50%);
transition: filter 0.2s;
}
button:has(.${reasoningModelCls}):hover .${iconColorWhiteCls} {
filter: invert(100%);
}
.${iconColorGoldCls} {
filter: brightness(0) saturate(100%) invert(88%) sepia(75%) saturate(2577%) hue-rotate(324deg) brightness(96%) contrast(99%);
transition: filter 0.2s;
}
`;
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. \`\`. See palette bellow.
- \`tooltip\`: shown on hover (aka title); (default) tooltip can be disabled when this field is set to empty string - \`\`
- \`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. \`\`, can be used for collections. only one link per tag is supported.
- \`link-target\`: target of the link, e.g. \`\` (opens in new tab), default is \`_self\` (same tab).
- \`icon\`: Lucide icon name, e.g. \`\`. 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-model\`: set the model, e.g. \`\`
- \`set-sources\`: set the sources, e.g. \`\` 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. \`\`
- \`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-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 -
, prefer concise modern syntax and style,
tell me a joke
tell me a joke
\`\`\`
Directory example:
\`\`\`
Games
FFXIV:
Vintage Story -
\`\`\`
Fence example:
\`\`\`
Shounen
Seinen
Shoujo
\`\`\`
Another fence example:
\`\`\`
Haskell
Raku
\`\`\`
`.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(``)
.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('')
.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('
')
.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('')
.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('')}`)
.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(``).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('')
.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('')
.addClass(tagFenceContentCls)
.css({
'width': fence.fenceWidth ?? '',
})
;
fenceEl.append(fenceContentEl);
return { fenceEl, fenceContentEl };
};
const createDirectory = () => {
const directoryEl = jq('').addClass(tagDirectoryCls);
const directoryContentEl = jq('').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('').addClass(tagAllFencesWrapperCls);
const restWrapperEl = jq('').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,
hideDiscoverButton: false,
fixImageGenerationOverlay: false,
extraSpaceBellowLastAnswer: false,
modelLabelTextMode: MODEL_LABEL_TEXT_MODE.OFF,
modelLabelStyle: MODEL_LABEL_STYLE.OFF,
modelLabelOverwriteCyanIconToGray: false,
modelLabelUseIconForReasoningModels: false,
modelLabelReasoningModelIconGold: false,
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(``);
const label = jq(``);
const checkboxWithLabel = jq('').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(``);
const bookIconHtml = `
`;
const labelTextHtml = `${labelText}`;
const label = jq(``);
const labelWithLinks = jq('').addClass('flex flex-row gap-2 mb-2').append(label);
const textareaWrapper = jq('').append(labelWithLinks);
if (links) {
links.forEach(({icon, label, url, tooltip}) => {
const iconHtml = `
`;
const link = jq(`${icon ? iconHtml : ''}${label ? ' ' + label : ''}`);
link.attr('title', tooltip);
labelWithLinks.append(link);
});
}
if (helpText) {
const help = jq(``).addClass(helpTextCls).html(markdownConverter.makeHtml(helpText)).append(jq('
'));
help.find('a').each((_, a) => jq(a).attr('target', '_blank'));
help.append(jq('').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(`